cogsbox-state 0.5.315 → 0.5.317

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.315",
3
+ "version": "0.5.317",
4
4
  "description": "React state management library with form controls and server sync",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/CogsState.tsx CHANGED
@@ -1814,16 +1814,24 @@ function createProxyHandler<T>(
1814
1814
  endIndex: 10,
1815
1815
  });
1816
1816
 
1817
- // This ref tracks if the user is locked to the bottom.
1818
1817
  const isLockedToBottomRef = useRef(stickToBottom);
1819
-
1820
- // This state triggers a re-render when item heights change.
1821
1818
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1822
1819
 
1820
+ // Track if we've scrolled to bottom after initial load
1821
+ const hasScrolledToBottomRef = useRef(false);
1822
+ const lastTotalCountRef = useRef(0);
1823
+
1823
1824
  useEffect(() => {
1825
+ let updateCount = 0;
1824
1826
  const unsubscribe = getGlobalStore
1825
1827
  .getState()
1826
1828
  .subscribeToShadowState(stateKey, () => {
1829
+ updateCount++;
1830
+ if (updateCount <= 5) {
1831
+ console.log(
1832
+ `[VirtualView] Shadow update #${updateCount}`
1833
+ );
1834
+ }
1827
1835
  setShadowUpdateTrigger((prev) => prev + 1);
1828
1836
  });
1829
1837
  return unsubscribe;
@@ -1835,32 +1843,77 @@ function createProxyHandler<T>(
1835
1843
  ) as any[];
1836
1844
  const totalCount = sourceArray.length;
1837
1845
 
1838
- // Calculate heights from shadow state. This runs when data or measurements change.
1839
- const { totalHeight, positions } = useMemo(() => {
1840
- const shadowArray =
1841
- getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1842
- [];
1843
- let height = 0;
1844
- const pos: number[] = [];
1845
- for (let i = 0; i < totalCount; i++) {
1846
- pos[i] = height;
1847
- const measuredHeight =
1848
- shadowArray[i]?.virtualizer?.itemHeight;
1849
- height += measuredHeight || itemHeight;
1850
- }
1851
- return { totalHeight: height, positions: pos };
1852
- }, [
1853
- totalCount,
1854
- stateKey,
1855
- path.join("."),
1856
- itemHeight,
1857
- shadowUpdateTrigger,
1858
- ]);
1846
+ console.log(
1847
+ `[VirtualView] Initial setup - totalCount: ${totalCount}, itemHeight: ${itemHeight}, stickToBottom: ${stickToBottom}`
1848
+ );
1849
+
1850
+ // Reset when array size changes significantly
1851
+ if (totalCount !== lastTotalCountRef.current) {
1852
+ console.log(
1853
+ `[VirtualView] Array size changed from ${lastTotalCountRef.current} to ${totalCount}`
1854
+ );
1855
+ hasScrolledToBottomRef.current = false;
1856
+ lastTotalCountRef.current = totalCount;
1857
+ }
1858
+
1859
+ // Calculate heights from shadow state
1860
+ const { totalHeight, positions, visibleMeasured } =
1861
+ useMemo(() => {
1862
+ const shadowArray =
1863
+ getGlobalStore
1864
+ .getState()
1865
+ .getShadowMetadata(stateKey, path) || [];
1866
+ let height = 0;
1867
+ const pos: number[] = [];
1868
+ let measuredCount = 0;
1869
+ let visibleMeasuredCount = 0;
1870
+
1871
+ for (let i = 0; i < totalCount; i++) {
1872
+ pos[i] = height;
1873
+ const measuredHeight =
1874
+ shadowArray[i]?.virtualizer?.itemHeight;
1875
+ if (measuredHeight) {
1876
+ measuredCount++;
1877
+ // Count measured items in current visible range
1878
+ if (i >= range.startIndex && i < range.endIndex) {
1879
+ visibleMeasuredCount++;
1880
+ }
1881
+ }
1882
+ height += measuredHeight || itemHeight;
1883
+ }
1884
+
1885
+ // Check if all VISIBLE items are measured
1886
+ const visibleCount = range.endIndex - range.startIndex;
1887
+ const allVisibleMeasured =
1888
+ visibleMeasuredCount === visibleCount && visibleCount > 0;
1889
+
1890
+ console.log(
1891
+ `[VirtualView] Heights calc - measured: ${measuredCount}/${totalCount}, visible measured: ${visibleMeasuredCount}/${visibleCount}, totalHeight: ${height}`
1892
+ );
1859
1893
 
1860
- // Memoize the virtualized slice of data.
1894
+ return {
1895
+ totalHeight: height,
1896
+ positions: pos,
1897
+ visibleMeasured: allVisibleMeasured,
1898
+ };
1899
+ }, [
1900
+ totalCount,
1901
+ stateKey,
1902
+ path.join("."),
1903
+ itemHeight,
1904
+ shadowUpdateTrigger,
1905
+ range, // Add range dependency
1906
+ ]);
1907
+
1908
+ // Memoize the virtualized slice
1861
1909
  const virtualState = useMemo(() => {
1862
1910
  const start = Math.max(0, range.startIndex);
1863
1911
  const end = Math.min(totalCount, range.endIndex);
1912
+
1913
+ console.log(
1914
+ `[VirtualView] Creating virtual slice - range: ${start}-${end} (${end - start} items)`
1915
+ );
1916
+
1864
1917
  const validIndices = Array.from(
1865
1918
  { length: end - start },
1866
1919
  (_, i) => start + i
@@ -1872,14 +1925,11 @@ function createProxyHandler<T>(
1872
1925
  });
1873
1926
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1874
1927
 
1875
- // This is the main effect that handles all scrolling and updates.
1928
+ // Main layout effect
1876
1929
  useLayoutEffect(() => {
1877
1930
  const container = containerRef.current;
1878
1931
  if (!container) return;
1879
1932
 
1880
- let scrollTimeoutId: NodeJS.Timeout;
1881
-
1882
- // This function determines what's visible in the viewport.
1883
1933
  const updateVirtualRange = () => {
1884
1934
  if (!container) return;
1885
1935
  const { scrollTop } = container;
@@ -1903,7 +1953,6 @@ function createProxyHandler<T>(
1903
1953
  setRange({ startIndex, endIndex });
1904
1954
  };
1905
1955
 
1906
- // This function handles ONLY user-initiated scrolls.
1907
1956
  const handleUserScroll = () => {
1908
1957
  isLockedToBottomRef.current =
1909
1958
  container.scrollHeight -
@@ -1917,32 +1966,55 @@ function createProxyHandler<T>(
1917
1966
  passive: true,
1918
1967
  });
1919
1968
 
1920
- // --- THE CORE FIX ---
1921
- if (stickToBottom) {
1922
- // We use a timeout to wait for React to render AND for useMeasure to update heights.
1923
- // This is the CRUCIAL part that fixes the race condition.
1924
- scrollTimeoutId = setTimeout(() => {
1925
- // By the time this runs, `container.scrollHeight` is accurate.
1926
- // We only scroll if the user hasn't manually scrolled up in the meantime.
1927
- if (isLockedToBottomRef.current) {
1969
+ // For stick to bottom: first jump to approximate bottom position
1970
+ if (
1971
+ stickToBottom &&
1972
+ !hasScrolledToBottomRef.current &&
1973
+ totalCount > 0
1974
+ ) {
1975
+ if (visibleMeasured || range.endIndex === totalCount) {
1976
+ // If we're showing the last items OR current visible items are measured
1977
+ console.log(
1978
+ `[VirtualView] Scrolling to bottom - visible measured: ${visibleMeasured}, at end: ${range.endIndex === totalCount}`
1979
+ );
1980
+ hasScrolledToBottomRef.current = true;
1981
+
1982
+ // Use setTimeout to ensure DOM updates are complete
1983
+ setTimeout(() => {
1928
1984
  container.scrollTo({
1929
- top: container.scrollHeight,
1930
- behavior: "auto", // ALWAYS 'auto' for an instant, correct jump.
1985
+ top: container.scrollHeight + 1000,
1986
+ behavior: "auto",
1931
1987
  });
1932
- }
1933
- }, 200); // A small 50ms delay is a robust buffer.
1988
+ isLockedToBottomRef.current = true;
1989
+ }, 0);
1990
+ } else {
1991
+ // Jump close to the bottom to trigger rendering of bottom items
1992
+ console.log(
1993
+ `[VirtualView] Jumping near bottom to trigger measurements`
1994
+ );
1995
+ const estimatedScrollPosition = Math.max(
1996
+ 0,
1997
+ (totalCount - 20) * itemHeight
1998
+ );
1999
+ container.scrollTo({
2000
+ top: estimatedScrollPosition,
2001
+ behavior: "auto",
2002
+ });
2003
+ }
1934
2004
  }
1935
2005
 
1936
- // Update the visible range on initial load.
1937
2006
  updateVirtualRange();
1938
2007
 
1939
- // Cleanup function is vital to prevent memory leaks.
1940
2008
  return () => {
1941
- clearTimeout(scrollTimeoutId);
1942
2009
  container.removeEventListener("scroll", handleUserScroll);
1943
2010
  };
1944
- // This effect re-runs whenever the list size or item heights change.
1945
- }, [totalCount, positions, stickToBottom]);
2011
+ }, [
2012
+ totalCount,
2013
+ positions,
2014
+ stickToBottom,
2015
+ visibleMeasured,
2016
+ range.endIndex,
2017
+ ]);
1946
2018
 
1947
2019
  const scrollToBottom = useCallback(
1948
2020
  (behavior: ScrollBehavior = "smooth") => {