Vấn đề gồm hai phần độc lập: dữ liệu đã tải và vị trí cuộn. Chỉ khôi phục một trong hai là vẫn sai.
Dữ liệu. Danh sách infinite scroll thường tải 5–6 trang; quay lại mà fetch từ trang 1 thì trang ngắn lại, không thể cuộn về chỗ cũ. Cần cache toàn bộ các trang theo key của query. Đây chính là việc useInfiniteQuery làm: khi component mount lại, dữ liệu cũ trong cache được trả về ngay, các trang đã tải vẫn còn nguyên.
Vị trí cuộn. Tắt cơ chế tự động của trình duyệt rồi tự lưu:
js
history.scrollRestoration = 'manual'
// before navigating away
sessionStorage.setItem('feed:scroll', String(window.scrollY))
// after the cached list has painted
useLayoutEffect(() => {
const y = Number(sessionStorage.getItem('feed:scroll') || 0)
if (y && items.length) window.scrollTo(0, y)
}, [items.length])Dùng useLayoutEffect và chỉ khôi phục sau khi danh sách đã render đủ chiều cao — gọi scrollTo khi trang còn ngắn thì trình duyệt sẽ kẹp giá trị về đáy hiện tại.
Các điểm dễ sai:
- Ảnh chưa có kích thước làm nội dung dịch chuyển sau khi khôi phục. Đặt
width/heighthoặcaspect-ratiocho mọi ảnh trong list. - Sentinel của
IntersectionObserverđang nằm trong viewport ngay khi khôi phục sẽ kích hoạt tải thêm ngoài ý muốn — chỉ gắn observer sau khi khôi phục xong. - Chỉ khôi phục khi điều hướng kiểu pop (Back/Forward). Vào lại từ link ngoài thì phải bắt đầu từ đầu.
- Với danh sách rất dài, cân nhắc virtualization; khi đó khôi phục theo index của item đầu tiên thay vì theo pixel.