Save current scroll of a div to local storage, React

Viewed 394

I've seen numerous questions on this topic but no one of them seem to go about it my way. They usually involve window scrolling. I have a div with items that can overflow and I want to remember the scroll that the user left. I save this in the local storage but I've been unable to get the correct scroll position. (The items are in hundreds)

const ItemHolder = ({
    shouldScroll, // this indicates whether or not we should load the previous scroll from the storage
    searchParams,
}: ItemHolderProps) => {
    const [items, setItems] = useImmer<Item[]>([]);

    const scrollRef = useRef<HTMLDivElement>(null);

 
        useEffect(() => { // HERE WE SHOULD GET THE SCROLL VALUE WHEN UNMOUNTING
        const scroll = scrollRef.current; // this is because we don't have access to ref.current in unmount

        return () => {
            if (scroll) {
                // doesn't work
                const winScroll = scroll.scrollTop;
                const height = scroll.scrollHeight - scroll.clientHeight;

                const scrolled = winScroll / height;

                localStorage.setItem("scroll", String(scrolled));
            }
        };
    }, []);

    useEffect(() => {
        const scroll = scrollRef.current;
        const scrollAmountStorage = localStorage.getItem("scroll");

        let scrollTop = 0;
        if (scrollAmountStorage) {
            scrollTop = Number(scrollAmountStorage);
        }

        if (shouldScroll && scroll) {
            console.log("Scroll top : ", scrollTop);
            scroll.scrollTo({ top: scrollTop });
        }
    }, [shouldScroll]);

    return (
        <div
            // onScroll={(e) => {
            //     console.log(e);
            // }}
            ref={scrollRef}
            className="grid grid-cols-2 mx-auto p-2 gap-3 overflow-x-auto h-full"
        >
            {items.map((i) => (
                <Item key={i.id} />
            ))}
        </div>
    );
};

Basically what confuses me is that there is no intuitive and straightforward way to get the current scroll and then just apply it again. All of the SO answers I've found used these kinds of calculations with windows.height and such.

2 Answers

I think this part is un-necessary, unless I totally missed your ask

                // doesn't work
                const height = scroll.scrollHeight - scroll.clientHeight;

                const scrolled = winScroll / height;

You were on the right track with scrollTop

Because the example above is not fully reproducible, including usage of Immer etc. I wrote a simple and clunky example that does what you want.

https://codesandbox.io/s/elegant-leakey-6rqv6?file=/index.html

Lots of cleanup possible and needs to be mapped to your code.

 const hugeDiv = document.querySelectorAll("#hugeDiv")[0];
    const storedScrollPosition = localStorage.getItem("scrollPos")
      ? localStorage.getItem("scrollPos")
      : 0;
    hugeDiv.scrollTo(0, storedScrollPosition);

    // Efficient scroll capture with throttle
    // Reference: http://www.html5rocks.com/en/tutorials/speed/animations/

    let lastKnownScrollPosition = 0;
    let ticking = false;

    function doSomething(scrollPos) {
      // Do something with the scroll position
      localStorage.setItem("scrollPos", lastKnownScrollPosition);
    }

    hugeDiv.addEventListener("scroll", function (e) {
      lastKnownScrollPosition = hugeDiv.scrollTop;

      if (!ticking) {
        window.requestAnimationFrame(function () {
          doSomething(lastKnownScrollPosition);
          ticking = false;
        });

        ticking = true;
      }
    });

Some considerations

  1. You can store scrollLeft for horizontal position as well
  2. Incognito does not store localstorage items.

I have made a very simple example for you here

We can use onScroll event to handle and get current scroll position for a div but we don't want to update state everytime so we can create a function for debounce the scroll event (so it will update the state when user stop scrolling)

function useDebounce(delay = 500) {
  const [data, setData] = useState(null);
  const [dataQuery, setDataQuery] = useState(null);

  useEffect(() => {
    const delayFn = setTimeout(() => setData(dataQuery), delay);
    return () => clearTimeout(delayFn);
  }, [dataQuery, delay]);

  return [data, setDataQuery];
}

And we will use it like this:

const [scrollPosition, setScrollPosition] = useDebounce();

On the div which we want to store scroll position:

<div
   ref={scrollRef}
   onScroll={({ target }) => setScrollPosition(target.scrollTop)}
/>

Then we will create a useEffect for to store the value whenever scrollPosition changed:

useEffect(() => {
    if (scrollPosition) localStorage.setItem("scrollPosition", scrollPosition);
}, [scrollPosition]);

And lastly we will create useEffect for initial render to change position of scroll;

useEffect(() => {
    let scrollPosition = localStorage.getItem("scrollPosition");
    if (scrollPosition) scrollRef.current.scrollTop = scrollPosition;
}, []);

And the overall code should look like this;

import { useEffect, useRef, useState } from "react";

function useDebounce(delay = 500) {
  const [data, setData] = useState(null);
  const [dataQuery, setDataQuery] = useState(null);

  useEffect(() => {
    const delayFn = setTimeout(() => setData(dataQuery), delay);
    return () => clearTimeout(delayFn);
  }, [dataQuery, delay]);

  return [data, setDataQuery];
}

export default function App() {
  const scrollRef = useRef(null);
  const [scrollPosition, setScrollPosition] = useDebounce();

  useEffect(() => {
    let scrollPosition = localStorage.getItem("scrollPosition");
    if (scrollPosition) scrollRef.current.scrollTop = scrollPosition;
  }, []);

  useEffect(() => {
    if (scrollPosition) localStorage.setItem("scrollPosition", scrollPosition);
  }, [scrollPosition]);

  return (
    <div className="App">
      <div
        ref={scrollRef}
        style={{ width: "200px", height: "200px", overflow: "auto" }}
        onScroll={({ target }) => {
          setScrollPosition(target.scrollTop);
        }}
      >
        <div style={{ width: "100%", height: "15000px" }} />
      </div>
    </div>
  );
}
Related