react-simple-virtualize 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,7 @@ const MyLargeList = () => {
51
51
  // Outer Container: Needs fixed height & overflow-y: auto
52
52
  <div
53
53
  onScroll={onScroll}
54
+ ref={scrollRef}
54
55
  style={{
55
56
  height: containerHeight,
56
57
  overflowY: 'auto',
@@ -65,6 +66,7 @@ const MyLargeList = () => {
65
66
  {virtualItems.map(({ index, offsetTop }) => (
66
67
  <div
67
68
  key={index}
69
+ ref={virtualItem.measureRef}
68
70
  style={{
69
71
  position: 'absolute',
70
72
  top: 0,
@@ -91,31 +93,64 @@ export default MyLargeList;
91
93
 
92
94
  ## ⚙️ API Reference
93
95
 
94
- ### `useVirtualize(options)`
96
+ | Prop | Type | Default | Description |
97
+ |---|---|---|---|
98
+ | `itemCount` | `number` | **Required** | Total number of items in the list. |
99
+ | `itemHeight` | `number` | **Required** | Estimated height of an item. Used for initial rendering and unmeasured items. |
100
+ | `overscan` | `number` | `3` | Number of extra items to render outside the visible viewport. |
101
+ | `containerHeight` | `number` | `undefined` | Optional fixed height. If omitted, the hook uses `ResizeObserver` on `scrollRef` to measure it automatically. |
95
102
 
96
- | Option | Type | Required | Description |
97
- | :--- | :--- | :--- | :--- |
98
- | `itemCount` | `number` | Yes | Total number of items in the list. |
99
- | `itemHeight` | `number` | Yes | Height of a single item in pixels. |
100
- | `containerHeight` | `number` | Yes | Height of the visible scroll area (viewport). |
101
- | `overscan` | `number` | No | Number of extra items to render above/below the visible area (Default: 3). |
102
-
103
- ### Returns
103
+ ### Return Values
104
104
 
105
105
  | Value | Type | Description |
106
- | :--- | :--- | :--- |
107
- | `virtualItems` | `array` | Array of items to be rendered. Each item contains `{ index, offsetTop }`. |
108
- | `totalHeight` | `number` | Total height of the list (used for the inner container). |
109
- | `onScroll` | `function` | Event handler to be attached to the outer container's `onScroll`. |
106
+ |---|---|---|
107
+ | `virtualItems` | `VirtualItem[]` | Array of items to be rendered in the current viewport. |
108
+ | `totalHeight` | `number` | Total calculated height of the list (apply this to the inner container). |
109
+ | `scrollRef` | `RefObject` | Ref to be attached to the scrollable container element (for auto-sizing). |
110
+ | `onScroll` | `function` | Scroll handler to be attached to the scrollable container. |
111
+
112
+ #### `VirtualItem` Interface
113
+ ```ts
114
+ interface VirtualItem {
115
+ index: number;
116
+ offsetTop: number;
117
+ measureRef: (el: HTMLElement | null) => void; // Must be attached to the rendered element
118
+ }
119
+ ```
120
+
121
+ ## Migration from v1 to v2
122
+
123
+ v2 introduces support for **Dynamic Heights**. This is a breaking change.
124
+
125
+ **Changes:**
126
+ - You must now attach the `measureRef` to your rendered element.
127
+ - `itemHeight` prop is now used as an **estimate** for unrendered items.
128
+
129
+ ```jsx
130
+ // v1 (Old)
131
+ <div style={{ transform: `translateY(${virtualItem.offsetTop}px)` }}>...</div>
132
+
133
+ // v2 (New)
134
+ <div
135
+ ref={virtualItem.measureRef} // <--- Required!
136
+ style={{ transform: `translateY(${virtualItem.offsetTop}px)` }}
137
+ >
138
+ ...
139
+ </div>
140
+ ```
110
141
 
111
142
  ## 🗺️ Roadmap & Support
112
143
 
113
- Currently, this package is optimized for **Fixed Height** lists.
114
- I am actively working on the following features for the next major release:
144
+ Here's what's planned for the future of `react-simple-virtualize`.
145
+
146
+ - [x] **Fixed Height Lists:** Initial release with core virtualization logic. (v1.0)
147
+ - [x] **Dynamic Height Lists:** Support for items with variable heights using a measurement ref. (v2.0) 🚀
148
+ - [ ] **Horizontal Virtualization:** Support for row-based scrolling.
149
+ - [ ] **Grid Virtualization:** Virtualize both rows and columns (spreadsheet style).
150
+ - [ ] **Window Scrolling:** Support for using the browser's native scrollbar instead of a container.
151
+ - [ ] **Infinite Loader:** Built-in support for loading more data as you scroll (Pagination).
115
152
 
116
- - [ ] **Dynamic Height Support:** For items with variable content (chat bubbles, feeds).
117
- - [ ] **Grid Virtualization:** Virtualizing both rows and columns.
118
- - [ ] **Horizontal Scrolling:** Support for X-axis virtualization.
153
+ > **Note:** If you have a specific feature request, feel free to open an issue!
119
154
 
120
155
  ### ☕ Support the Development
121
156
 
@@ -0,0 +1,27 @@
1
+ import { UIEvent } from "react";
2
+ export interface UseVirtualizeProps {
3
+ /** Total number of items in the list */
4
+ itemCount: number;
5
+ /** Estimated height of a single item (used for unmeasured items) */
6
+ itemHeight: number;
7
+ /** Number of extra items to render outside the visible view (default: 3) */
8
+ overscan?: number;
9
+ /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */
10
+ containerHeight?: number;
11
+ }
12
+ export interface VirtualItem {
13
+ /** The index of the item in the original data array */
14
+ index: number;
15
+ /** The calculated top position (px) for absolute positioning */
16
+ offsetTop: number;
17
+ /** Callback ref to measure the actual DOM height of the item */
18
+ measureRef: (el: HTMLElement | null) => void;
19
+ }
20
+ export declare function useVirtualize({ itemCount, itemHeight: estimatedItemHeight, overscan, containerHeight: propsContainerHeight, }: UseVirtualizeProps): {
21
+ virtualItems: VirtualItem[];
22
+ totalHeight: number;
23
+ scrollRef: import("react").RefObject<HTMLDivElement | null>;
24
+ measurementVersion: number;
25
+ onScroll: (e: UIEvent<HTMLElement>) => void;
26
+ };
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,OAAO,EACR,MAAM,OAAO,CAAC;AAEf,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,UAAU,EAAE,mBAAmB,EAC/B,QAAY,EACZ,eAAe,EAAE,oBAAoB,GACtC,EAAE,kBAAkB;;;;;kBAyLH,QAAQ,WAAW,CAAC;EAGrC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var t=require("react");exports.useVirtualize=function(e){var r=e.itemCount,o=e.itemHeight,a=e.containerHeight,i=e.overscan,n=void 0===i?3:i,u=t.useState(0),l=u[0],h=u[1],s=t.useMemo(function(){for(var t=Math.floor(l/o),e=Math.min(r-1,Math.floor((l+a)/o)),i=Math.max(0,t-n),u=Math.min(r-1,e+n),h=[],s=i;s<=u;s++)h.push({index:s,offsetTop:s*o});return{virtualItems:h,totalHeight:r*o}},[l,r,o,a,n]);return{virtualItems:s.virtualItems,totalHeight:s.totalHeight,onScroll:function(t){h(t.currentTarget.scrollTop)}}};
1
+ var e=require("react");exports.useVirtualize=function(r){var t=r.itemCount,n=r.itemHeight,u=r.overscan,c=void 0===u?3:u,i=r.containerHeight,a=e.useState(0),o=a[0],f=a[1],s=e.useState(0),l=s[0],v=s[1],h=null!=i?i:l,R=e.useRef({}),m=e.useRef({}),g=e.useRef(-1),b=e.useRef(null),d=e.useRef(0),C=e.useRef(0),M=e.useState(0),k=M[0],p=M[1],x=e.useCallback(function(){return C.current>0?d.current/C.current:n},[n]);e.useLayoutEffect(function(){if(void 0===i){var e=b.current;if(e){var r=new ResizeObserver(function(e){var r=e[0];r.contentRect.height!==l&&v(r.contentRect.height)});return r.observe(e),function(){return r.disconnect()}}}},[i,l]);var S=e.useCallback(function(e){if(e<=g.current)return m.current[e]||0;var r=g.current;return(r>=0?m.current[r]:0)+(r>=0?R.current[r]:0)+(e-r-1)*x()},[n,x]),H=e.useCallback(function(e){for(var r=0,n=t-1;r<=n;){var u=Math.floor((r+n)/2),c=S(u),i=R.current[u]||x();if(c<=e&&c+i>e)return u;c<e?r=u+1:n=u-1}return Math.max(0,r-1)},[S,t,n]),T=e.useCallback(function(e){return function(r){if(r){var t=r.getBoundingClientRect().height,n=R.current[e];if(n!==t){if(void 0===n?(d.current+=t,C.current+=1):d.current+=t-n,R.current[e]=t,e>g.current){var u=S(e);m.current[e]=u,g.current=e}else g.current=Math.min(g.current,e-1);p(function(e){return e+1})}}}},[S]),z=H(o),V=Math.min(t-1,H(o+h)+c);return{virtualItems:e.useMemo(function(){for(var e=[],r=Math.max(0,z-c);r<=V;r++)e.push({index:r,offsetTop:S(r),measureRef:T(r)});return e},[z,V,S,T,c]),totalHeight:S(t),scrollRef:b,measurementVersion:k,onScroll:function(e){return f(e.currentTarget.scrollTop)}}};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["_ref","itemCount","itemHeight","containerHeight","_ref$overscan","overscan","_useState","useState","scrollTop","setScrollTop","_useMemo","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","virtualItems","i","push","index","offsetTop","totalHeight","onScroll","e","currentTarget"],"mappings":"6CAE6B,SAAHA,GAKpB,IAJJC,EAASD,EAATC,UACAC,EAAUF,EAAVE,WACAC,EAAeH,EAAfG,gBAAeC,EAAAJ,EACfK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EAGZE,EAAkCC,EAAQA,SAAC,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAG9B,GAKAI,EAAsCC,EAAAA,QAAQ,WAc5C,IAZA,IAAMC,EAAaC,KAAKC,MAAMN,EAAYN,GACpCa,EAAWF,KAAKG,IACpBf,EAAY,EACZY,KAAKC,OAAON,EAAYL,GAAmBD,IAIvCe,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAIf,EAAY,EAAGc,EAAWV,GAG9Ce,EAAe,GACZC,EAAIJ,EAAYI,GAAKF,EAAUE,IACtCD,EAAaE,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAInB,IAOnB,MAAO,CAAEkB,aAAAA,EAAcK,YAFHxB,EAAYC,EAGlC,EAAG,CAACM,EAAWP,EAAWC,EAAYC,EAAiBE,IAEvD,MAAO,CACLe,aA5BkBV,EAAZU,aA6BNK,YA7B+Bf,EAAXe,YA8BpBC,SAnCe,SAACC,GAChBlB,EAAakB,EAAEC,cAAcpB,UAC/B,EAmCF"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["_ref","itemCount","estimatedItemHeight","itemHeight","_ref$overscan","overscan","propsContainerHeight","containerHeight","_useState","useState","scrollTop","setScrollTop","_useState2","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","_useState3","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","_ref2","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{useState as t,useMemo as o}from"react";const e=({itemCount:e,itemHeight:r,containerHeight:a,overscan:i=3})=>{const[n,l]=t(0),{virtualItems:h,totalHeight:m}=o(()=>{const t=Math.floor(n/r),o=Math.min(e-1,Math.floor((n+a)/r)),l=Math.max(0,t-i),h=Math.min(e-1,o+i),m=[];for(let t=l;t<=h;t++)m.push({index:t,offsetTop:t*r});return{virtualItems:m,totalHeight:e*r}},[n,e,r,a,i]);return{virtualItems:h,totalHeight:m,onScroll:t=>{l(t.currentTarget.scrollTop)}}};export{e as useVirtualize};
1
+ import{useState as r,useRef as t,useCallback as e,useLayoutEffect as n,useMemo as c}from"react";function u({itemCount:u,itemHeight:o,overscan:i=3,containerHeight:s}){const[l,f]=r(0),[a,h]=r(0),m=null!=s?s:a,g=t({}),v=t({}),R=t(-1),d=t(null),p=t(0),M=t(0),[x,H]=r(0),T=e(()=>M.current>0?p.current/M.current:o,[o]);n(()=>{if(void 0!==s)return;const r=d.current;if(!r)return;const t=new ResizeObserver(([r])=>{r.contentRect.height!==a&&h(r.contentRect.height)});return t.observe(r),()=>t.disconnect()},[s,a]);const b=e(r=>{if(r<=R.current)return v.current[r]||0;const t=R.current;return(t>=0?v.current[t]:0)+(t>=0?g.current[t]:0)+(r-t-1)*T()},[o,T]),C=e(r=>{let t=0,e=u-1;for(;t<=e;){const n=Math.floor((t+e)/2),c=b(n),u=g.current[n]||T();if(c<=r&&c+u>r)return n;c<r?t=n+1:e=n-1}return Math.max(0,t-1)},[b,u,o]),w=e(r=>t=>{if(!t)return;const e=t.getBoundingClientRect().height,n=g.current[r];if(n!==e){if(void 0===n?(p.current+=e,M.current+=1):p.current+=e-n,g.current[r]=e,r>R.current){const t=b(r);v.current[r]=t,R.current=r}else R.current=Math.min(R.current,r-1);H(r=>r+1)}},[b]),z=C(l),B=Math.min(u-1,C(l+m)+i);return{virtualItems:c(()=>{const r=[];for(let t=Math.max(0,z-i);t<=B;t++)r.push({index:t,offsetTop:b(t),measureRef:w(t)});return r},[z,B,b,w,i]),totalHeight:b(u),scrollRef:d,measurementVersion:x,onScroll:r=>f(r.currentTarget.scrollTop)}}export{u as useVirtualize};
2
2
  //# sourceMappingURL=index.modern.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.modern.mjs","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["useVirtualize","itemCount","itemHeight","containerHeight","overscan","scrollTop","setScrollTop","useState","virtualItems","totalHeight","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","i","push","index","offsetTop","onScroll","e","currentTarget"],"mappings":"8CAEa,MAAAA,EAAgBA,EAC3BC,YACAC,aACAC,kBACAC,SAAAA,EAAW,MAGX,MAAOC,EAAWC,GAAgBC,EAAS,IAQrCC,aAAEA,EAAYC,YAAEA,GAAgBC,EAAQ,KAE5C,MAAMC,EAAaC,KAAKC,MAAMR,EAAYH,GACpCY,EAAWF,KAAKG,IACpBd,EAAY,EACZW,KAAKC,OAAOR,EAAYF,GAAmBD,IAIvCc,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAId,EAAY,EAAGa,EAAWV,GAG9CI,EAAe,GACrB,IAAK,IAAIW,EAAIH,EAAYG,GAAKD,EAAUC,IACtCX,EAAaY,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAIjB,IAOnB,MAAO,CAAEM,eAAcC,YAFHR,EAAYC,IAG/B,CAACG,EAAWJ,EAAWC,EAAYC,EAAiBC,IAEvD,MAAO,CACLI,eACAC,cACAc,SAnCgBC,IAChBlB,EAAakB,EAAEC,cAAcpB"}
1
+ {"version":3,"file":"index.modern.mjs","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["useVirtualize","itemCount","itemHeight","estimatedItemHeight","overscan","containerHeight","propsContainerHeight","scrollTop","setScrollTop","useState","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{useState as t,useMemo as o}from"react";var r=function(r){var e=r.itemCount,a=r.itemHeight,i=r.containerHeight,n=r.overscan,l=void 0===n?3:n,h=t(0),u=h[0],m=h[1],c=o(function(){for(var t=Math.floor(u/a),o=Math.min(e-1,Math.floor((u+i)/a)),r=Math.max(0,t-l),n=Math.min(e-1,o+l),h=[],m=r;m<=n;m++)h.push({index:m,offsetTop:m*a});return{virtualItems:h,totalHeight:e*a}},[u,e,a,i,l]);return{virtualItems:c.virtualItems,totalHeight:c.totalHeight,onScroll:function(t){m(t.currentTarget.scrollTop)}}};export{r as useVirtualize};
1
+ import{useState as r,useRef as t,useCallback as n,useLayoutEffect as e,useMemo as u}from"react";function c(c){var i=c.itemCount,o=c.itemHeight,f=c.overscan,a=void 0===f?3:f,v=c.containerHeight,h=r(0),l=h[0],s=h[1],m=r(0),g=m[0],d=m[1],R=null!=v?v:g,p=t({}),M=t({}),x=t(-1),H=t(null),T=t(0),b=t(0),C=r(0),w=C[0],z=C[1],B=n(function(){return b.current>0?T.current/b.current:o},[o]);e(function(){if(void 0===v){var r=H.current;if(r){var t=new ResizeObserver(function(r){var t=r[0];t.contentRect.height!==g&&d(t.contentRect.height)});return t.observe(r),function(){return t.disconnect()}}}},[v,g]);var I=n(function(r){if(r<=x.current)return M.current[r]||0;var t=x.current;return(t>=0?M.current[t]:0)+(t>=0?p.current[t]:0)+(r-t-1)*B()},[o,B]),O=n(function(r){for(var t=0,n=i-1;t<=n;){var e=Math.floor((t+n)/2),u=I(e),c=p.current[e]||B();if(u<=r&&u+c>r)return e;u<r?t=e+1:n=e-1}return Math.max(0,t-1)},[I,i,o]),S=n(function(r){return function(t){if(t){var n=t.getBoundingClientRect().height,e=p.current[r];if(e!==n){if(void 0===e?(T.current+=n,b.current+=1):T.current+=n-e,p.current[r]=n,r>x.current){var u=I(r);M.current[r]=u,x.current=r}else x.current=Math.min(x.current,r-1);z(function(r){return r+1})}}}},[I]),V=O(l),j=Math.min(i-1,O(l+R)+a);return{virtualItems:u(function(){for(var r=[],t=Math.max(0,V-a);t<=j;t++)r.push({index:t,offsetTop:I(t),measureRef:S(t)});return r},[V,j,I,S,a]),totalHeight:I(i),scrollRef:H,measurementVersion:w,onScroll:function(r){return s(r.currentTarget.scrollTop)}}}export{c as useVirtualize};
2
2
  //# sourceMappingURL=index.module.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.module.js","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["useVirtualize","_ref","itemCount","itemHeight","containerHeight","_ref$overscan","overscan","_useState","useState","scrollTop","setScrollTop","_useMemo","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","virtualItems","i","push","index","offsetTop","totalHeight","onScroll","e","currentTarget"],"mappings":"8CAEa,IAAAA,EAAgB,SAAHC,GAKpB,IAJJC,EAASD,EAATC,UACAC,EAAUF,EAAVE,WACAC,EAAeH,EAAfG,gBAAeC,EAAAJ,EACfK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EAGZE,EAAkCC,EAAS,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAG9B,GAKAI,EAAsCC,EAAQ,WAc5C,IAZA,IAAMC,EAAaC,KAAKC,MAAMN,EAAYN,GACpCa,EAAWF,KAAKG,IACpBf,EAAY,EACZY,KAAKC,OAAON,EAAYL,GAAmBD,IAIvCe,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAIf,EAAY,EAAGc,EAAWV,GAG9Ce,EAAe,GACZC,EAAIJ,EAAYI,GAAKF,EAAUE,IACtCD,EAAaE,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAInB,IAOnB,MAAO,CAAEkB,aAAAA,EAAcK,YAFHxB,EAAYC,EAGlC,EAAG,CAACM,EAAWP,EAAWC,EAAYC,EAAiBE,IAEvD,MAAO,CACLe,aA5BkBV,EAAZU,aA6BNK,YA7B+Bf,EAAXe,YA8BpBC,SAnCe,SAACC,GAChBlB,EAAakB,EAAEC,cAAcpB,UAC/B,EAmCF"}
1
+ {"version":3,"file":"index.module.js","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["useVirtualize","_ref","itemCount","estimatedItemHeight","itemHeight","_ref$overscan","overscan","propsContainerHeight","containerHeight","_useState","useState","scrollTop","setScrollTop","_useState2","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","_useState3","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","_ref2","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"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"}
package/dist/index.umd.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],e):e((t||self).reactSimpleVirtualize={},t.react)}(this,function(t,e){t.useVirtualize=function(t){var o=t.itemCount,i=t.itemHeight,n=t.containerHeight,r=t.overscan,a=void 0===r?3:r,u=e.useState(0),f=u[0],l=u[1],s=e.useMemo(function(){for(var t=Math.floor(f/i),e=Math.min(o-1,Math.floor((f+n)/i)),r=Math.max(0,t-a),u=Math.min(o-1,e+a),l=[],s=r;s<=u;s++)l.push({index:s,offsetTop:s*i});return{virtualItems:l,totalHeight:o*i}},[f,o,i,n,a]);return{virtualItems:s.virtualItems,totalHeight:s.totalHeight,onScroll:function(t){l(t.currentTarget.scrollTop)}}}});
1
+ !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],r):r((e||self).reactSimpleVirtualize={},e.react)}(this,function(e,r){e.useVirtualize=function(e){var t=e.itemCount,n=e.itemHeight,u=e.overscan,c=void 0===u?3:u,i=e.containerHeight,o=r.useState(0),f=o[0],a=o[1],s=r.useState(0),l=s[0],h=s[1],v=null!=i?i:l,d=r.useRef({}),m=r.useRef({}),R=r.useRef(-1),g=r.useRef(null),p=r.useRef(0),b=r.useRef(0),x=r.useState(0),C=x[0],M=x[1],y=r.useCallback(function(){return b.current>0?p.current/b.current:n},[n]);r.useLayoutEffect(function(){if(void 0===i){var e=g.current;if(e){var r=new ResizeObserver(function(e){var r=e[0];r.contentRect.height!==l&&h(r.contentRect.height)});return r.observe(e),function(){return r.disconnect()}}}},[i,l]);var S=r.useCallback(function(e){if(e<=R.current)return m.current[e]||0;var r=R.current;return(r>=0?m.current[r]:0)+(r>=0?d.current[r]:0)+(e-r-1)*y()},[n,y]),T=r.useCallback(function(e){for(var r=0,n=t-1;r<=n;){var u=Math.floor((r+n)/2),c=S(u),i=d.current[u]||y();if(c<=e&&c+i>e)return u;c<e?r=u+1:n=u-1}return Math.max(0,r-1)},[S,t,n]),k=r.useCallback(function(e){return function(r){if(r){var t=r.getBoundingClientRect().height,n=d.current[e];if(n!==t){if(void 0===n?(p.current+=t,b.current+=1):p.current+=t-n,d.current[e]=t,e>R.current){var u=S(e);m.current[e]=u,R.current=e}else R.current=Math.min(R.current,e-1);M(function(e){return e+1})}}}},[S]),z=T(f),H=Math.min(t-1,T(f+v)+c);return{virtualItems:r.useMemo(function(){for(var e=[],r=Math.max(0,z-c);r<=H;r++)e.push({index:r,offsetTop:S(r),measureRef:k(r)});return e},[z,H,S,k,c]),totalHeight:S(t),scrollRef:g,measurementVersion:C,onScroll:function(e){return a(e.currentTarget.scrollTop)}}}});
2
2
  //# sourceMappingURL=index.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.umd.js","sources":["../src/index.js"],"sourcesContent":["import { useState, useMemo } from 'react';\n\nexport const useVirtualize = ({\n itemCount,\n itemHeight,\n containerHeight,\n overscan = 3\n}) => {\n // Store the current scroll position\n const [scrollTop, setScrollTop] = useState(0);\n\n // Update scroll position on scroll event\n const onScroll = (e) => {\n setScrollTop(e.currentTarget.scrollTop);\n };\n\n // Memoize calculations to prevent unnecessary re-renders\n const { virtualItems, totalHeight } = useMemo(() => {\n // 1. Calculate the visible range based on scroll position\n const rangeStart = Math.floor(scrollTop / itemHeight);\n const rangeEnd = Math.min(\n itemCount - 1,\n Math.floor((scrollTop + containerHeight) / itemHeight)\n );\n\n // 2. Add overscan (buffer) to the range for smoother scrolling\n const startIndex = Math.max(0, rangeStart - overscan);\n const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);\n\n // 3. Generate the array of items to be rendered\n const virtualItems = [];\n for (let i = startIndex; i <= endIndex; i++) {\n virtualItems.push({\n index: i,\n offsetTop: i * itemHeight, // Calculate absolute position for each item\n });\n }\n\n // 4. Calculate total phantom height to maintain scrollbar size\n const totalHeight = itemCount * itemHeight;\n\n return { virtualItems, totalHeight };\n }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);\n\n return {\n virtualItems,\n totalHeight,\n onScroll,\n };\n};"],"names":["_ref","itemCount","itemHeight","containerHeight","_ref$overscan","overscan","_useState","useState","scrollTop","setScrollTop","_useMemo","useMemo","rangeStart","Math","floor","rangeEnd","min","startIndex","max","endIndex","virtualItems","i","push","index","offsetTop","totalHeight","onScroll","e","currentTarget"],"mappings":"kSAE6B,SAAHA,GAKpB,IAJJC,EAASD,EAATC,UACAC,EAAUF,EAAVE,WACAC,EAAeH,EAAfG,gBAAeC,EAAAJ,EACfK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EAGZE,EAAkCC,EAAQA,SAAC,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAG9B,GAKAI,EAAsCC,EAAAA,QAAQ,WAc5C,IAZA,IAAMC,EAAaC,KAAKC,MAAMN,EAAYN,GACpCa,EAAWF,KAAKG,IACpBf,EAAY,EACZY,KAAKC,OAAON,EAAYL,GAAmBD,IAIvCe,EAAaJ,KAAKK,IAAI,EAAGN,EAAaP,GACtCc,EAAWN,KAAKG,IAAIf,EAAY,EAAGc,EAAWV,GAG9Ce,EAAe,GACZC,EAAIJ,EAAYI,GAAKF,EAAUE,IACtCD,EAAaE,KAAK,CAChBC,MAAOF,EACPG,UAAWH,EAAInB,IAOnB,MAAO,CAAEkB,aAAAA,EAAcK,YAFHxB,EAAYC,EAGlC,EAAG,CAACM,EAAWP,EAAWC,EAAYC,EAAiBE,IAEvD,MAAO,CACLe,aA5BkBV,EAAZU,aA6BNK,YA7B+Bf,EAAXe,YA8BpBC,SAnCe,SAACC,GAChBlB,EAAakB,EAAEC,cAAcpB,UAC/B,EAmCF"}
1
+ {"version":3,"file":"index.umd.js","sources":["../src/index.ts"],"sourcesContent":["import {\n useState,\n useRef,\n useMemo,\n useCallback,\n useLayoutEffect,\n UIEvent,\n} from \"react\";\n\nexport interface UseVirtualizeProps {\n /** Total number of items in the list */\n itemCount: number;\n /** Estimated height of a single item (used for unmeasured items) */\n itemHeight: number;\n /** Number of extra items to render outside the visible view (default: 3) */\n overscan?: number;\n /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */\n containerHeight?: number;\n}\n\nexport interface VirtualItem {\n /** The index of the item in the original data array */\n index: number;\n /** The calculated top position (px) for absolute positioning */\n offsetTop: number;\n /** Callback ref to measure the actual DOM height of the item */\n measureRef: (el: HTMLElement | null) => void;\n}\n\nexport function useVirtualize({\n itemCount,\n itemHeight: estimatedItemHeight,\n overscan = 3,\n containerHeight: propsContainerHeight,\n}: UseVirtualizeProps) {\n const [scrollTop, setScrollTop] = useState(0);\n const [observedHeight, setObservedHeight] = useState(0);\n\n // Determine the effective container height:\n // Use the prop if provided; otherwise, fall back to the measured height.\n const height = propsContainerHeight ?? observedHeight;\n\n // --- REFS (State that doesn't trigger re-renders) ---\n\n // Stores the exact height of each measured item: { index: height }\n const measurementCache = useRef({} as Record<number, number>);\n\n // Stores the calculated total offset for each index: { index: offset }\n // This acts as a Prefix Sum array to avoid recalculating from 0 every time.\n const offsetCache = useRef({} as Record<number, number>);\n\n // Tracks the last index that has a valid calculated offset in the cache.\n const lastMeasuredIndex = useRef(-1);\n\n // Reference to the scrollable container (used for ResizeObserver)\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Total measured height (Pixel)\n const totalMeasuredSize = useRef(0);\n // Total measured element?\n const totalMeasuredCount = useRef(0);\n\n const [measurementVersion, setMeasurementVersion] = useState(0);\n\n // --- HELPER: GET ESTIMATED HEIGHT ---\n // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.\n // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.\n const getEstimatedItemHeight = useCallback(() => {\n return totalMeasuredCount.current > 0\n ? totalMeasuredSize.current / totalMeasuredCount.current\n : estimatedItemHeight;\n }, [estimatedItemHeight]);\n\n // --- 1. AUTO-SIZER LOGIC ---\n useLayoutEffect(() => {\n // If the user provided a fixed height, skip observation to save resources.\n if (propsContainerHeight !== undefined) return;\n\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const observer = new ResizeObserver(([entry]) => {\n // Update state only if dimensions actually changed\n if (entry.contentRect.height !== observedHeight) {\n setObservedHeight(entry.contentRect.height);\n }\n });\n\n observer.observe(scrollElement);\n\n return () => observer.disconnect();\n }, [propsContainerHeight, observedHeight]);\n\n // --- 2. OFFSET CALCULATOR (Core Logic) ---\n // Returns the vertical position (px) of an item.\n const getItemOffset = useCallback(\n (index: number) => {\n // If the offset for this index is already cached, return it directly.\n if (index <= lastMeasuredIndex.current) {\n return offsetCache.current[index] || 0;\n }\n\n // 1. Retrieve reference data from the last measured item.\n const lastIndex = lastMeasuredIndex.current;\n\n // If no items have been measured yet (-1), the starting offset is 0.\n // Otherwise, retrieve the offset of the last measured item.\n const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;\n\n // Retrieve the height of the last measured item (default to 0 if none measured).\n const lastHeight =\n lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;\n\n // 2. Calculate the bottom position of the last measured item.\n // This serves as the anchor point for estimating subsequent offsets.\n const lastBottom = lastOffset + lastHeight;\n\n // 3. Calculate how many unmeasured items exist between the last measured index and the target index.\n const unmeasuredCount = index - lastIndex - 1;\n\n // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)\n return lastBottom + unmeasuredCount * getEstimatedItemHeight();\n },\n [estimatedItemHeight, getEstimatedItemHeight]\n );\n\n // --- 3. BINARY SEARCH (Find Visible Index) ---\n // Finds the index corresponding to the current scroll position.\n // Performance: O(log N) - Crucial for large lists.\n const getStartIndexForOffset = useCallback(\n (offset: number) => {\n let low = 0;\n let high = itemCount - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const currentOffset = getItemOffset(mid);\n const currentHeight =\n measurementCache.current[mid] || getEstimatedItemHeight();\n\n if (currentOffset <= offset && currentOffset + currentHeight > offset) {\n return mid;\n } else if (currentOffset < offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n // Fallback to 0 if not found\n return Math.max(0, low - 1);\n },\n [getItemOffset, itemCount, estimatedItemHeight]\n );\n\n // --- 4. MEASUREMENT CALLBACK (The \"Ref\" logic) ---\n // This function is passed to the consumer to attach to their DOM elements.\n const measureElement = useCallback(\n (index: number) => (el: HTMLElement | null) => {\n if (!el) return;\n\n const measuredHeight = el.getBoundingClientRect().height;\n const prevHeight = measurementCache.current[index];\n\n if (prevHeight !== measuredHeight) {\n if (prevHeight === undefined) {\n totalMeasuredSize.current += measuredHeight;\n totalMeasuredCount.current += 1;\n } else {\n totalMeasuredSize.current += measuredHeight - prevHeight;\n }\n\n measurementCache.current[index] = measuredHeight;\n\n if (index > lastMeasuredIndex.current) {\n const prevOffset = getItemOffset(index);\n offsetCache.current[index] = prevOffset;\n lastMeasuredIndex.current = index;\n } else {\n lastMeasuredIndex.current = Math.min(\n lastMeasuredIndex.current,\n index - 1\n );\n }\n\n setMeasurementVersion(v => v + 1);\n }\n },\n [getItemOffset]\n );\n\n // --- 5. RENDER RANGE CALCULATION ---\n const startIndex = getStartIndexForOffset(scrollTop);\n const endIndex = Math.min(\n itemCount - 1,\n getStartIndexForOffset(scrollTop + height) + overscan\n );\n\n // Generate the virtual items array to be rendered\n const virtualItems = useMemo(() => {\n const items: VirtualItem[] = [];\n for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {\n items.push({\n index: i,\n offsetTop: getItemOffset(i),\n measureRef: measureElement(i),\n });\n }\n return items;\n }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);\n\n // Calculate total list height for the scrollbar\n const totalHeight = getItemOffset(itemCount);\n\n return {\n virtualItems,\n totalHeight,\n scrollRef,\n measurementVersion,\n onScroll: (e: UIEvent<HTMLElement>) =>\n setScrollTop(e.currentTarget.scrollTop),\n };\n}\n"],"names":["_ref","itemCount","estimatedItemHeight","itemHeight","_ref$overscan","overscan","propsContainerHeight","containerHeight","_useState","useState","scrollTop","setScrollTop","_useState2","observedHeight","setObservedHeight","height","measurementCache","useRef","offsetCache","lastMeasuredIndex","scrollRef","totalMeasuredSize","totalMeasuredCount","_useState3","measurementVersion","setMeasurementVersion","getEstimatedItemHeight","useCallback","current","useLayoutEffect","undefined","scrollElement","observer","ResizeObserver","_ref2","entry","contentRect","observe","disconnect","getItemOffset","index","lastIndex","getStartIndexForOffset","offset","low","high","mid","Math","floor","currentOffset","currentHeight","max","measureElement","el","measuredHeight","getBoundingClientRect","prevHeight","prevOffset","min","v","startIndex","endIndex","virtualItems","useMemo","items","i","push","offsetTop","measureRef","totalHeight","onScroll","e","currentTarget"],"mappings":"2SA6B6BA,GAKR,IAJnBC,EAASD,EAATC,UACYC,EAAmBF,EAA/BG,WAAUC,EAAAJ,EACVK,SAAAA,OAAQ,IAAAD,EAAG,EAACA,EACKE,EAAoBN,EAArCO,gBAEAC,EAAkCC,EAAQA,SAAC,GAApCC,EAASF,EAAEG,GAAAA,EAAYH,EAAA,GAC9BI,EAA4CH,WAAS,GAA9CI,EAAcD,EAAEE,GAAAA,EAAiBF,EAAA,GAIlCG,EAAST,MAAAA,EAAAA,EAAwBO,EAKjCG,EAAmBC,SAAO,CAAA,GAI1BC,EAAcD,EAAAA,OAAO,CAA4B,GAGjDE,EAAoBF,EAAMA,QAAE,GAG5BG,EAAYH,SAAuB,MAGnCI,EAAoBJ,EAAAA,OAAO,GAE3BK,EAAqBL,EAAMA,OAAC,GAElCM,EAAoDd,EAAAA,SAAS,GAAtDe,EAAkBD,EAAEE,GAAAA,EAAqBF,KAK1CG,EAAyBC,EAAWA,YAAC,WACzC,OAAOL,EAAmBM,QAAU,EAChCP,EAAkBO,QAAUN,EAAmBM,QAC/C1B,CACN,EAAG,CAACA,IAGJ2B,EAAAA,gBAAgB,WAEd,QAA6BC,IAAzBxB,EAAJ,CAEA,IAAMyB,EAAgBX,EAAUQ,QAChC,GAAKG,EAAL,CAEA,IAAMC,EAAW,IAAIC,eAAe,SAAAC,GAAE,IAAAC,EAAKD,KAErCC,EAAMC,YAAYrB,SAAWF,GAC/BC,EAAkBqB,EAAMC,YAAYrB,OAExC,GAIA,OAFAiB,EAASK,QAAQN,GAEJ,WAAA,OAAAC,EAASM,YAAY,GACpC,EAAG,CAAChC,EAAsBO,IAI1B,IAAM0B,EAAgBZ,EAAAA,YACpB,SAACa,GAEC,GAAIA,GAASrB,EAAkBS,QAC7B,OAAOV,EAAYU,QAAQY,IAAU,EAIvC,IAAMC,EAAYtB,EAAkBS,QAkBpC,OAdmBa,GAAa,EAAIvB,EAAYU,QAAQa,GAAa,IAInEA,GAAa,EAAIzB,EAAiBY,QAAQa,GAAa,IAOjCD,EAAQC,EAAY,GAGNf,GACxC,EACA,CAACxB,EAAqBwB,IAMlBgB,EAAyBf,EAAAA,YAC7B,SAACgB,GAIC,IAHA,IAAIC,EAAM,EACNC,EAAO5C,EAAY,EAEhB2C,GAAOC,GAAM,CAClB,IAAMC,EAAMC,KAAKC,OAAOJ,EAAMC,GAAQ,GAChCI,EAAgBV,EAAcO,GAC9BI,EACJlC,EAAiBY,QAAQkB,IAAQpB,IAEnC,GAAIuB,GAAiBN,GAAUM,EAAgBC,EAAgBP,EAC7D,OAAOG,EACEG,EAAgBN,EACzBC,EAAME,EAAM,EAEZD,EAAOC,EAAM,CAEhB,CAGD,OAAOC,KAAKI,IAAI,EAAGP,EAAM,EAC3B,EACA,CAACL,EAAetC,EAAWC,IAKvBkD,EAAiBzB,cACrB,SAACa,GAAkB,OAAA,SAACa,GAClB,GAAKA,EAAL,CAEA,IAAMC,EAAiBD,EAAGE,wBAAwBxC,OAC5CyC,EAAaxC,EAAiBY,QAAQY,GAE5C,GAAIgB,IAAeF,EAAgB,CAUjC,QATmBxB,IAAf0B,GACFnC,EAAkBO,SAAW0B,EAC7BhC,EAAmBM,SAAW,GAE9BP,EAAkBO,SAAW0B,EAAiBE,EAGhDxC,EAAiBY,QAAQY,GAASc,EAE9Bd,EAAQrB,EAAkBS,QAAS,CACrC,IAAM6B,EAAalB,EAAcC,GACjCtB,EAAYU,QAAQY,GAASiB,EAC7BtC,EAAkBS,QAAUY,CAC7B,MACCrB,EAAkBS,QAAUmB,KAAKW,IAC/BvC,EAAkBS,QAClBY,EAAQ,GAIZf,EAAsB,SAAAkC,GAAK,OAAAA,EAAI,CAAC,EACjC,EACH,CAAC,EACD,CAACpB,IAIGqB,EAAalB,EAAuBhC,GACpCmD,EAAWd,KAAKW,IACpBzD,EAAY,EACZyC,EAAuBhC,EAAYK,GAAUV,GAmB/C,MAAO,CACLyD,aAhBmBC,EAAAA,QAAQ,WAE3B,IADA,IAAMC,EAAuB,GACpBC,EAAIlB,KAAKI,IAAI,EAAGS,EAAavD,GAAW4D,GAAKJ,EAAUI,IAC9DD,EAAME,KAAK,CACT1B,MAAOyB,EACPE,UAAW5B,EAAc0B,GACzBG,WAAYhB,EAAea,KAG/B,OAAOD,CACT,EAAG,CAACJ,EAAYC,EAAUtB,EAAea,EAAgB/C,IAOvDgE,YAJkB9B,EAActC,GAKhCmB,UAAAA,EACAI,mBAAAA,EACA8C,SAAU,SAACC,GACT,OAAA5D,EAAa4D,EAAEC,cAAc9D,UAAU,EAE7C"}
package/package.json CHANGED
@@ -1,39 +1,45 @@
1
1
  {
2
- "name": "react-simple-virtualize",
3
- "version": "1.0.0",
4
- "description": "A lightweight, dependency-free virtualization hook for React.",
5
- "source": "src/index.js",
6
- "main": "dist/index.js",
7
- "module": "dist/index.module.js",
8
- "unpkg": "dist/index.umd.js",
9
- "types": "src/index.d.ts",
10
- "scripts": {
11
- "build": "microbundle",
12
- "dev": "microbundle watch",
13
- "prepublishOnly": "npm run build"
14
- },
15
- "keywords": [
16
- "react",
17
- "virtual-list",
18
- "virtualization",
19
- "scroll",
20
- "performance",
21
- "hook"
22
- ],
23
- "author": "Ozan Batuhan Ceylan",
24
- "license": "MIT",
25
- "repository": {
26
- "type": "git",
27
- "url": "https://github.com/ozanbatuhanceylan/react-simple-virtualize"
28
- },
29
- "funding": {
30
- "type": "buymeacoffee",
31
- "url": "https://www.buymeacoffee.com/obceylan"
32
- },
33
- "peerDependencies": {
34
- "react": ">=16.8.0"
35
- },
36
- "devDependencies": {
37
- "microbundle": "^0.15.1"
38
- }
39
- }
2
+ "name": "react-simple-virtualize",
3
+ "version": "2.0.0",
4
+ "description": "A lightweight, dependency-free virtualization hook for React.",
5
+ "source": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.module.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.module.js"
13
+ },
14
+ "unpkg": "dist/index.umd.js",
15
+ "scripts": {
16
+ "build": "microbundle",
17
+ "dev": "microbundle watch",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "react",
22
+ "virtual-list",
23
+ "virtualization",
24
+ "scroll",
25
+ "performance",
26
+ "hook"
27
+ ],
28
+ "author": "Ozan Batuhan Ceylan",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/ozanbatuhanceylan/react-simple-virtualize"
33
+ },
34
+ "funding": {
35
+ "type": "buymeacoffee",
36
+ "url": "https://www.buymeacoffee.com/obceylan"
37
+ },
38
+ "peerDependencies": {
39
+ "react": ">=16.8.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/react": "^19.2.8",
43
+ "microbundle": "^0.15.1"
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,223 @@
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useMemo,
5
+ useCallback,
6
+ useLayoutEffect,
7
+ UIEvent,
8
+ } from "react";
9
+
10
+ export interface UseVirtualizeProps {
11
+ /** Total number of items in the list */
12
+ itemCount: number;
13
+ /** Estimated height of a single item (used for unmeasured items) */
14
+ itemHeight: number;
15
+ /** Number of extra items to render outside the visible view (default: 3) */
16
+ overscan?: number;
17
+ /** Fixed container height. If not provided, it will be measured automatically via ResizeObserver */
18
+ containerHeight?: number;
19
+ }
20
+
21
+ export interface VirtualItem {
22
+ /** The index of the item in the original data array */
23
+ index: number;
24
+ /** The calculated top position (px) for absolute positioning */
25
+ offsetTop: number;
26
+ /** Callback ref to measure the actual DOM height of the item */
27
+ measureRef: (el: HTMLElement | null) => void;
28
+ }
29
+
30
+ export function useVirtualize({
31
+ itemCount,
32
+ itemHeight: estimatedItemHeight,
33
+ overscan = 3,
34
+ containerHeight: propsContainerHeight,
35
+ }: UseVirtualizeProps) {
36
+ const [scrollTop, setScrollTop] = useState(0);
37
+ const [observedHeight, setObservedHeight] = useState(0);
38
+
39
+ // Determine the effective container height:
40
+ // Use the prop if provided; otherwise, fall back to the measured height.
41
+ const height = propsContainerHeight ?? observedHeight;
42
+
43
+ // --- REFS (State that doesn't trigger re-renders) ---
44
+
45
+ // Stores the exact height of each measured item: { index: height }
46
+ const measurementCache = useRef({} as Record<number, number>);
47
+
48
+ // Stores the calculated total offset for each index: { index: offset }
49
+ // This acts as a Prefix Sum array to avoid recalculating from 0 every time.
50
+ const offsetCache = useRef({} as Record<number, number>);
51
+
52
+ // Tracks the last index that has a valid calculated offset in the cache.
53
+ const lastMeasuredIndex = useRef(-1);
54
+
55
+ // Reference to the scrollable container (used for ResizeObserver)
56
+ const scrollRef = useRef<HTMLDivElement>(null);
57
+
58
+ // Total measured height (Pixel)
59
+ const totalMeasuredSize = useRef(0);
60
+ // Total measured element?
61
+ const totalMeasuredCount = useRef(0);
62
+
63
+ const [measurementVersion, setMeasurementVersion] = useState(0);
64
+
65
+ // --- HELPER: GET ESTIMATED HEIGHT ---
66
+ // Zeka burada: Eğer hiç ölçüm yoksa senin verdiğin 'itemHeight'ı kullan.
67
+ // Ama ölçüm yaptıysak, gerçek ortalamayı kullan.
68
+ const getEstimatedItemHeight = useCallback(() => {
69
+ return totalMeasuredCount.current > 0
70
+ ? totalMeasuredSize.current / totalMeasuredCount.current
71
+ : estimatedItemHeight;
72
+ }, [estimatedItemHeight]);
73
+
74
+ // --- 1. AUTO-SIZER LOGIC ---
75
+ useLayoutEffect(() => {
76
+ // If the user provided a fixed height, skip observation to save resources.
77
+ if (propsContainerHeight !== undefined) return;
78
+
79
+ const scrollElement = scrollRef.current;
80
+ if (!scrollElement) return;
81
+
82
+ const observer = new ResizeObserver(([entry]) => {
83
+ // Update state only if dimensions actually changed
84
+ if (entry.contentRect.height !== observedHeight) {
85
+ setObservedHeight(entry.contentRect.height);
86
+ }
87
+ });
88
+
89
+ observer.observe(scrollElement);
90
+
91
+ return () => observer.disconnect();
92
+ }, [propsContainerHeight, observedHeight]);
93
+
94
+ // --- 2. OFFSET CALCULATOR (Core Logic) ---
95
+ // Returns the vertical position (px) of an item.
96
+ const getItemOffset = useCallback(
97
+ (index: number) => {
98
+ // If the offset for this index is already cached, return it directly.
99
+ if (index <= lastMeasuredIndex.current) {
100
+ return offsetCache.current[index] || 0;
101
+ }
102
+
103
+ // 1. Retrieve reference data from the last measured item.
104
+ const lastIndex = lastMeasuredIndex.current;
105
+
106
+ // If no items have been measured yet (-1), the starting offset is 0.
107
+ // Otherwise, retrieve the offset of the last measured item.
108
+ const lastOffset = lastIndex >= 0 ? offsetCache.current[lastIndex] : 0;
109
+
110
+ // Retrieve the height of the last measured item (default to 0 if none measured).
111
+ const lastHeight =
112
+ lastIndex >= 0 ? measurementCache.current[lastIndex] : 0;
113
+
114
+ // 2. Calculate the bottom position of the last measured item.
115
+ // This serves as the anchor point for estimating subsequent offsets.
116
+ const lastBottom = lastOffset + lastHeight;
117
+
118
+ // 3. Calculate how many unmeasured items exist between the last measured index and the target index.
119
+ const unmeasuredCount = index - lastIndex - 1;
120
+
121
+ // 4. Result: Anchor Point + (Number of Unmeasured Items * Estimated Height)
122
+ return lastBottom + unmeasuredCount * getEstimatedItemHeight();
123
+ },
124
+ [estimatedItemHeight, getEstimatedItemHeight]
125
+ );
126
+
127
+ // --- 3. BINARY SEARCH (Find Visible Index) ---
128
+ // Finds the index corresponding to the current scroll position.
129
+ // Performance: O(log N) - Crucial for large lists.
130
+ const getStartIndexForOffset = useCallback(
131
+ (offset: number) => {
132
+ let low = 0;
133
+ let high = itemCount - 1;
134
+
135
+ while (low <= high) {
136
+ const mid = Math.floor((low + high) / 2);
137
+ const currentOffset = getItemOffset(mid);
138
+ const currentHeight =
139
+ measurementCache.current[mid] || getEstimatedItemHeight();
140
+
141
+ if (currentOffset <= offset && currentOffset + currentHeight > offset) {
142
+ return mid;
143
+ } else if (currentOffset < offset) {
144
+ low = mid + 1;
145
+ } else {
146
+ high = mid - 1;
147
+ }
148
+ }
149
+
150
+ // Fallback to 0 if not found
151
+ return Math.max(0, low - 1);
152
+ },
153
+ [getItemOffset, itemCount, estimatedItemHeight]
154
+ );
155
+
156
+ // --- 4. MEASUREMENT CALLBACK (The "Ref" logic) ---
157
+ // This function is passed to the consumer to attach to their DOM elements.
158
+ const measureElement = useCallback(
159
+ (index: number) => (el: HTMLElement | null) => {
160
+ if (!el) return;
161
+
162
+ const measuredHeight = el.getBoundingClientRect().height;
163
+ const prevHeight = measurementCache.current[index];
164
+
165
+ if (prevHeight !== measuredHeight) {
166
+ if (prevHeight === undefined) {
167
+ totalMeasuredSize.current += measuredHeight;
168
+ totalMeasuredCount.current += 1;
169
+ } else {
170
+ totalMeasuredSize.current += measuredHeight - prevHeight;
171
+ }
172
+
173
+ measurementCache.current[index] = measuredHeight;
174
+
175
+ if (index > lastMeasuredIndex.current) {
176
+ const prevOffset = getItemOffset(index);
177
+ offsetCache.current[index] = prevOffset;
178
+ lastMeasuredIndex.current = index;
179
+ } else {
180
+ lastMeasuredIndex.current = Math.min(
181
+ lastMeasuredIndex.current,
182
+ index - 1
183
+ );
184
+ }
185
+
186
+ setMeasurementVersion(v => v + 1);
187
+ }
188
+ },
189
+ [getItemOffset]
190
+ );
191
+
192
+ // --- 5. RENDER RANGE CALCULATION ---
193
+ const startIndex = getStartIndexForOffset(scrollTop);
194
+ const endIndex = Math.min(
195
+ itemCount - 1,
196
+ getStartIndexForOffset(scrollTop + height) + overscan
197
+ );
198
+
199
+ // Generate the virtual items array to be rendered
200
+ const virtualItems = useMemo(() => {
201
+ const items: VirtualItem[] = [];
202
+ for (let i = Math.max(0, startIndex - overscan); i <= endIndex; i++) {
203
+ items.push({
204
+ index: i,
205
+ offsetTop: getItemOffset(i),
206
+ measureRef: measureElement(i),
207
+ });
208
+ }
209
+ return items;
210
+ }, [startIndex, endIndex, getItemOffset, measureElement, overscan]);
211
+
212
+ // Calculate total list height for the scrollbar
213
+ const totalHeight = getItemOffset(itemCount);
214
+
215
+ return {
216
+ virtualItems,
217
+ totalHeight,
218
+ scrollRef,
219
+ measurementVersion,
220
+ onScroll: (e: UIEvent<HTMLElement>) =>
221
+ setScrollTop(e.currentTarget.scrollTop),
222
+ };
223
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "lib": ["DOM", "ESNext"],
6
+ "moduleResolution": "node",
7
+ "jsx": "react-jsx",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "noImplicitAny": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": [
18
+ "src/**/*"
19
+ ],
20
+ "exclude": [
21
+ "node_modules",
22
+ "dist"
23
+ ]
24
+ }
package/src/index.js DELETED
@@ -1,50 +0,0 @@
1
- import { useState, useMemo } from 'react';
2
-
3
- export const useVirtualize = ({
4
- itemCount,
5
- itemHeight,
6
- containerHeight,
7
- overscan = 3
8
- }) => {
9
- // Store the current scroll position
10
- const [scrollTop, setScrollTop] = useState(0);
11
-
12
- // Update scroll position on scroll event
13
- const onScroll = (e) => {
14
- setScrollTop(e.currentTarget.scrollTop);
15
- };
16
-
17
- // Memoize calculations to prevent unnecessary re-renders
18
- const { virtualItems, totalHeight } = useMemo(() => {
19
- // 1. Calculate the visible range based on scroll position
20
- const rangeStart = Math.floor(scrollTop / itemHeight);
21
- const rangeEnd = Math.min(
22
- itemCount - 1,
23
- Math.floor((scrollTop + containerHeight) / itemHeight)
24
- );
25
-
26
- // 2. Add overscan (buffer) to the range for smoother scrolling
27
- const startIndex = Math.max(0, rangeStart - overscan);
28
- const endIndex = Math.min(itemCount - 1, rangeEnd + overscan);
29
-
30
- // 3. Generate the array of items to be rendered
31
- const virtualItems = [];
32
- for (let i = startIndex; i <= endIndex; i++) {
33
- virtualItems.push({
34
- index: i,
35
- offsetTop: i * itemHeight, // Calculate absolute position for each item
36
- });
37
- }
38
-
39
- // 4. Calculate total phantom height to maintain scrollbar size
40
- const totalHeight = itemCount * itemHeight;
41
-
42
- return { virtualItems, totalHeight };
43
- }, [scrollTop, itemCount, itemHeight, containerHeight, overscan]);
44
-
45
- return {
46
- virtualItems,
47
- totalHeight,
48
- onScroll,
49
- };
50
- };