cogsbox-state 0.5.312 → 0.5.314

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.312",
3
+ "version": "0.5.314",
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,22 +1814,18 @@ function createProxyHandler<T>(
1814
1814
  endIndex: 10,
1815
1815
  });
1816
1816
 
1817
- // --- State and Lock Management Refs ---
1817
+ // This ref tracks if the user is locked to the bottom.
1818
1818
  const isLockedToBottomRef = useRef(stickToBottom);
1819
- const isInitialMountRef = useRef(true);
1820
1819
 
1821
- // Subscribe to shadow state changes for height updates
1820
+ // This state triggers a re-render when item heights change.
1822
1821
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1823
1822
 
1824
1823
  useEffect(() => {
1825
- // Subscribe to shadow state updates for this stateKey
1826
1824
  const unsubscribe = getGlobalStore
1827
1825
  .getState()
1828
1826
  .subscribeToShadowState(stateKey, () => {
1829
- // Force recalculation when shadow state updates
1830
1827
  setShadowUpdateTrigger((prev) => prev + 1);
1831
1828
  });
1832
-
1833
1829
  return unsubscribe;
1834
1830
  }, [stateKey]);
1835
1831
 
@@ -1839,21 +1835,19 @@ function createProxyHandler<T>(
1839
1835
  ) as any[];
1840
1836
  const totalCount = sourceArray.length;
1841
1837
 
1842
- // Calculate heights from shadow state
1838
+ // Calculate heights from shadow state. This runs when data or measurements change.
1843
1839
  const { totalHeight, positions } = useMemo(() => {
1844
1840
  const shadowArray =
1845
1841
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1846
1842
  [];
1847
1843
  let height = 0;
1848
1844
  const pos: number[] = [];
1849
-
1850
1845
  for (let i = 0; i < totalCount; i++) {
1851
1846
  pos[i] = height;
1852
1847
  const measuredHeight =
1853
1848
  shadowArray[i]?.virtualizer?.itemHeight;
1854
1849
  height += measuredHeight || itemHeight;
1855
1850
  }
1856
-
1857
1851
  return { totalHeight: height, positions: pos };
1858
1852
  }, [
1859
1853
  totalCount,
@@ -1863,6 +1857,7 @@ function createProxyHandler<T>(
1863
1857
  shadowUpdateTrigger,
1864
1858
  ]);
1865
1859
 
1860
+ // Memoize the virtualized slice of data.
1866
1861
  const virtualState = useMemo(() => {
1867
1862
  const start = Math.max(0, range.startIndex);
1868
1863
  const end = Math.min(totalCount, range.endIndex);
@@ -1877,36 +1872,27 @@ function createProxyHandler<T>(
1877
1872
  });
1878
1873
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1879
1874
 
1875
+ // This is the main effect that handles all scrolling and updates.
1880
1876
  useLayoutEffect(() => {
1881
1877
  const container = containerRef.current;
1882
1878
  if (!container) return;
1883
1879
 
1884
- const handleScroll = () => {
1885
- const { scrollTop, clientHeight, scrollHeight } = container;
1886
-
1887
- // Determine if the user is at the bottom
1888
- const isNowAtBottom =
1889
- scrollHeight - scrollTop - clientHeight < 1;
1890
-
1891
- // If the user scrolls up, unlock. If they scroll back down, re-lock.
1892
- isLockedToBottomRef.current = isNowAtBottom;
1880
+ let scrollTimeoutId: NodeJS.Timeout;
1893
1881
 
1894
- // Binary search for start index
1882
+ // This function determines what's visible in the viewport.
1883
+ const updateVirtualRange = () => {
1884
+ if (!container) return;
1885
+ const { scrollTop } = container;
1895
1886
  let low = 0,
1896
1887
  high = totalCount - 1;
1897
1888
  while (low <= high) {
1898
1889
  const mid = Math.floor((low + high) / 2);
1899
- if (positions[mid]! < scrollTop) {
1900
- low = mid + 1;
1901
- } else {
1902
- high = mid - 1;
1903
- }
1890
+ if (positions[mid]! < scrollTop) low = mid + 1;
1891
+ else high = mid - 1;
1904
1892
  }
1905
1893
  const startIndex = Math.max(0, high - overscan);
1906
-
1907
- // Find end index
1908
1894
  let endIndex = startIndex;
1909
- const visibleEnd = scrollTop + clientHeight;
1895
+ const visibleEnd = scrollTop + container.clientHeight;
1910
1896
  while (
1911
1897
  endIndex < totalCount &&
1912
1898
  positions[endIndex]! < visibleEnd
@@ -1914,53 +1900,54 @@ function createProxyHandler<T>(
1914
1900
  endIndex++;
1915
1901
  }
1916
1902
  endIndex = Math.min(totalCount, endIndex + overscan);
1903
+ setRange({ startIndex, endIndex });
1904
+ };
1917
1905
 
1918
- setRange((prevRange) => {
1919
- if (
1920
- prevRange.startIndex !== startIndex ||
1921
- prevRange.endIndex !== endIndex
1922
- ) {
1923
- return { startIndex, endIndex };
1924
- }
1925
- return prevRange;
1926
- });
1906
+ // This function handles ONLY user-initiated scrolls.
1907
+ const handleUserScroll = () => {
1908
+ isLockedToBottomRef.current =
1909
+ container.scrollHeight -
1910
+ container.scrollTop -
1911
+ container.clientHeight <
1912
+ 1;
1913
+ updateVirtualRange();
1927
1914
  };
1928
1915
 
1929
- container.addEventListener("scroll", handleScroll, {
1916
+ container.addEventListener("scroll", handleUserScroll, {
1930
1917
  passive: true,
1931
1918
  });
1932
1919
 
1933
- // --- REFINED STICK-TO-BOTTOM LOGIC ---
1934
- if (stickToBottom && isLockedToBottomRef.current) {
1935
- // Use 'auto' for instant snap on first load, 'smooth' for subsequent updates.
1936
- const behavior = isInitialMountRef.current
1937
- ? "auto"
1938
- : "smooth";
1939
-
1940
- container.scrollTo({
1941
- top: container.scrollHeight,
1942
- behavior,
1943
- });
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) {
1928
+ container.scrollTo({
1929
+ top: container.scrollHeight,
1930
+ behavior: "auto", // ALWAYS 'auto' for an instant, correct jump.
1931
+ });
1932
+ }
1933
+ }, 50); // A small 50ms delay is a robust buffer.
1944
1934
  }
1945
1935
 
1946
- // After the first layout effect, it's no longer the initial mount.
1947
- // queueMicrotask ensures this is set *after* the current render cycle.
1948
- queueMicrotask(() => {
1949
- if (isInitialMountRef.current) {
1950
- isInitialMountRef.current = false;
1951
- }
1952
- });
1953
-
1954
- // Run handleScroll once on setup to set initial range and lock status
1955
- handleScroll();
1936
+ // Update the visible range on initial load.
1937
+ updateVirtualRange();
1956
1938
 
1957
- return () =>
1958
- container.removeEventListener("scroll", handleScroll);
1959
- }, [totalCount, positions, overscan, stickToBottom]);
1939
+ // Cleanup function is vital to prevent memory leaks.
1940
+ return () => {
1941
+ clearTimeout(scrollTimeoutId);
1942
+ container.removeEventListener("scroll", handleUserScroll);
1943
+ };
1944
+ // This effect re-runs whenever the list size or item heights change.
1945
+ }, [totalCount, positions, stickToBottom]);
1960
1946
 
1961
1947
  const scrollToBottom = useCallback(
1962
1948
  (behavior: ScrollBehavior = "smooth") => {
1963
1949
  if (containerRef.current) {
1950
+ isLockedToBottomRef.current = true;
1964
1951
  containerRef.current.scrollTo({
1965
1952
  top: containerRef.current.scrollHeight,
1966
1953
  behavior,
@@ -1973,6 +1960,7 @@ function createProxyHandler<T>(
1973
1960
  const scrollToIndex = useCallback(
1974
1961
  (index: number, behavior: ScrollBehavior = "smooth") => {
1975
1962
  if (containerRef.current && positions[index] !== undefined) {
1963
+ isLockedToBottomRef.current = false;
1976
1964
  containerRef.current.scrollTo({
1977
1965
  top: positions[index],
1978
1966
  behavior,
@@ -1999,6 +1987,7 @@ function createProxyHandler<T>(
1999
1987
  },
2000
1988
  },
2001
1989
  };
1990
+
2002
1991
  return {
2003
1992
  virtualState,
2004
1993
  virtualizerProps,