cogsbox-state 0.5.343 → 0.5.345

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.343",
3
+ "version": "0.5.345",
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
@@ -1804,19 +1804,33 @@ function createProxyHandler<T>(
1804
1804
  ): VirtualStateObjectResult<any[]> => {
1805
1805
  const {
1806
1806
  itemHeight = 50,
1807
- overscan = 6, // Keeping your working overscan value
1807
+ overscan = 6,
1808
1808
  stickToBottom = false,
1809
1809
  } = options;
1810
-
1810
+ const sourceArray = getGlobalStore().getNestedState(
1811
+ stateKey,
1812
+ path
1813
+ ) as any[];
1811
1814
  const containerRef = useRef<HTMLDivElement | null>(null);
1812
- const [range, setRange] = useState({
1813
- startIndex: 0,
1814
- endIndex: 10,
1815
- });
1816
- // The MASTER SWITCH for auto-scrolling. Starts true.
1815
+ // We'll set the range to the end first, then let an effect handle the scroll.
1816
+ const initialRange = () => {
1817
+ const sourceArray = getGlobalStore
1818
+ .getState()
1819
+ .getNestedState(stateKey, path) as any[];
1820
+ if (stickToBottom) {
1821
+ const visibleCount = 10; // A reasonable guess for initial render
1822
+ return {
1823
+ startIndex: Math.max(
1824
+ 0,
1825
+ sourceArray.length - visibleCount - overscan
1826
+ ),
1827
+ endIndex: sourceArray.length,
1828
+ };
1829
+ }
1830
+ return { startIndex: 0, endIndex: 10 };
1831
+ };
1832
+ const [range, setRange] = useState(initialRange);
1817
1833
  const isLockedToBottomRef = useRef(stickToBottom);
1818
- // Store the previous item count to detect when new items are added.
1819
- const prevTotalCountRef = useRef(0);
1820
1834
 
1821
1835
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1822
1836
 
@@ -1829,10 +1843,6 @@ function createProxyHandler<T>(
1829
1843
  return unsubscribe;
1830
1844
  }, [stateKey]);
1831
1845
 
1832
- const sourceArray = getGlobalStore().getNestedState(
1833
- stateKey,
1834
- path
1835
- ) as any[];
1836
1846
  const totalCount = sourceArray.length;
1837
1847
 
1838
1848
  const { totalHeight, positions } = useMemo(() => {
@@ -1870,16 +1880,48 @@ function createProxyHandler<T>(
1870
1880
  });
1871
1881
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1872
1882
 
1873
- // This layout effect is for SCROLLING and RANGE updates ONLY
1883
+ // This is the implementation of YOUR ALGORITHM.
1874
1884
  useLayoutEffect(() => {
1875
1885
  const container = containerRef.current;
1876
- if (!container) return;
1886
+ if (
1887
+ !container ||
1888
+ !stickToBottom ||
1889
+ !isLockedToBottomRef.current
1890
+ ) {
1891
+ return;
1892
+ }
1893
+
1894
+ // STEP 1: Check if the last item is measured. This is our "ready" signal.
1895
+ const lastItemIndex = totalCount - 1;
1896
+ const shadowArray =
1897
+ getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1898
+ [];
1899
+ const lastItemIsMeasured =
1900
+ lastItemIndex >= 0 &&
1901
+ shadowArray[lastItemIndex]?.virtualizer?.itemHeight > 0;
1902
+
1903
+ // STEP 2: If it's measured, we know totalHeight is correct. We can now scroll.
1904
+ if (lastItemIsMeasured || totalCount === 0) {
1905
+ // A timeout is essential for 'smooth' to work reliably after a render.
1906
+ const scrollTimeout = setTimeout(() => {
1907
+ container.scrollTo({
1908
+ top: container.scrollHeight,
1909
+ behavior: "smooth",
1910
+ });
1911
+ }, 50); // A small buffer is safer than 0ms.
1912
+
1913
+ return () => clearTimeout(scrollTimeout);
1914
+ }
1915
+ // If the last item is NOT measured, this effect does nothing and simply waits.
1916
+ // It will automatically re-run when the measurement comes in (via shadowUpdateTrigger).
1917
+ }, [totalCount, totalHeight, stickToBottom]); // Re-run when layout changes.
1877
1918
 
1878
- const hasNewItems = totalCount > prevTotalCountRef.current;
1919
+ // This effect ONLY handles user interaction and range updates.
1920
+ useEffect(() => {
1921
+ const container = containerRef.current;
1922
+ if (!container) return;
1879
1923
 
1880
- // This function determines what's visible in the viewport.
1881
1924
  const updateVirtualRange = () => {
1882
- if (!container) return;
1883
1925
  const { scrollTop, clientHeight } = container;
1884
1926
  let low = 0,
1885
1927
  high = totalCount - 1;
@@ -1897,53 +1939,13 @@ function createProxyHandler<T>(
1897
1939
  ) {
1898
1940
  endIndex++;
1899
1941
  }
1900
- endIndex = Math.min(totalCount, endIndex + overscan);
1901
- setRange({ startIndex, endIndex });
1942
+ setRange({
1943
+ startIndex,
1944
+ endIndex: Math.min(totalCount, endIndex + overscan),
1945
+ });
1902
1946
  };
1903
1947
 
1904
- // If new items were added AND we are locked to the bottom, scroll smoothly.
1905
- if (
1906
- stickToBottom &&
1907
- hasNewItems &&
1908
- isLockedToBottomRef.current
1909
- ) {
1910
- // A timeout is essential for smooth scrolling to work after a render.
1911
- const scrollTimeout = setTimeout(() => {
1912
- if (container) {
1913
- container.scrollTo({
1914
- top: container.scrollHeight,
1915
- behavior: "smooth",
1916
- });
1917
- }
1918
- }, 50); // A small buffer (50ms) is more robust for measurement to catch up.
1919
-
1920
- // Cleanup the timeout if the component unmounts or effect re-runs.
1921
- return () => clearTimeout(scrollTimeout);
1922
- }
1923
-
1924
- // Always update the visible range.
1925
- updateVirtualRange();
1926
- }, [
1927
- totalCount,
1928
- positions,
1929
- totalHeight,
1930
- stickToBottom,
1931
- itemHeight,
1932
- ]); // Dependencies for scrolling/range.
1933
-
1934
- // This effect is ONLY for handling USER SCROLLS and the initial scroll.
1935
- useEffect(() => {
1936
- const container = containerRef.current;
1937
- if (!container) return;
1938
-
1939
- // On initial mount, if we need to stick to the bottom, do it instantly.
1940
- if (isLockedToBottomRef.current) {
1941
- container.scrollTop = container.scrollHeight;
1942
- }
1943
-
1944
1948
  const handleUserScroll = () => {
1945
- if (!container) return;
1946
- // If the user scrolls up, break the lock.
1947
1949
  const isAtBottom =
1948
1950
  container.scrollHeight -
1949
1951
  container.scrollTop -
@@ -1952,24 +1954,22 @@ function createProxyHandler<T>(
1952
1954
  if (!isAtBottom) {
1953
1955
  isLockedToBottomRef.current = false;
1954
1956
  }
1957
+ updateVirtualRange();
1955
1958
  };
1956
1959
 
1957
1960
  container.addEventListener("scroll", handleUserScroll, {
1958
1961
  passive: true,
1959
1962
  });
1963
+ // Initial range calculation
1964
+ updateVirtualRange();
1965
+
1960
1966
  return () =>
1961
1967
  container.removeEventListener("scroll", handleUserScroll);
1962
- }, []); // This runs only once on mount.
1963
-
1964
- // After every render, update the previous count ref.
1965
- useEffect(() => {
1966
- prevTotalCountRef.current = totalCount;
1967
- });
1968
+ }, [totalCount, positions]);
1968
1969
 
1969
1970
  const scrollToBottom = useCallback(
1970
1971
  (behavior: ScrollBehavior = "smooth") => {
1971
1972
  if (containerRef.current) {
1972
- // Re-enable the lock when the user explicitly asks to scroll to bottom.
1973
1973
  isLockedToBottomRef.current = true;
1974
1974
  containerRef.current.scrollTo({
1975
1975
  top: containerRef.current.scrollHeight,
@@ -1983,7 +1983,6 @@ function createProxyHandler<T>(
1983
1983
  const scrollToIndex = useCallback(
1984
1984
  (index: number, behavior: ScrollBehavior = "smooth") => {
1985
1985
  if (containerRef.current && positions[index] !== undefined) {
1986
- // Scrolling to a specific index should break the lock.
1987
1986
  isLockedToBottomRef.current = false;
1988
1987
  containerRef.current.scrollTo({
1989
1988
  top: positions[index],