I have a car listing page built with React and React Router. Users can scroll down a list of cars, click a car to view its detail page,
and then hit the browser back button.
The problem: when returning, the page always scrolls to the top instead of the position where the user left off.
The list is dynamic: it loads asynchronously from Firestore, shuffles the cars randomly, and supports pagination and filters. Because of this, scroll restoration happens before the list is fully rendered, so the scroll jumps to the top instead of staying at the previous position.
Using window.history.scrollRestoration = “manual”.
A custom SmartScroll component that saves/restores scroll positions.
React Router v6 ScrollRestoration component.
Expected behavior:
When navigating back from a car detail page, the scroll should restore to the exact position the user left off, even with async data loading, shuffling, and pagination.
// src/main.jsx
import React from "react";
import "./index.css";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import SmartScroll from './utils/SmartScroll';
import { CartProvider } from "./context/CartContext";
import { WishlistProvider } from "./context/WishlistContext";
import { AuthProvider } from "./context/AuthContext";
import { SearchProvider } from "./context/SearchContext";
import { FilterProvider } from "./context/FilterContext";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<BrowserRouter>
<SmartScroll />
<AuthProvider>
<CartProvider>
<WishlistProvider>
<SearchProvider>
<FilterProvider>
<App />
</FilterProvider>
</SearchProvider>
</WishlistProvider>
</CartProvider>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
// src/App/jsx
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home/Home";
import Cart from "./pages/Cart";
import CarDetails from './pages/CarDetails';
import Navbar from "./components/layout/Navbar";
import Footer from "./components/layout/Footer";
import Checkout from "./pages/Checkout";
import Wishlist from "./pages/Wishlist";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import Success from "./pages/Success";
import About from "./pages/About";
import MyOrders from "./pages/MyOrders";
import ReceiptPage from "./pages/ReceiptPage";
import Inventory from "./pages/Inventory";
import SellYourCar from "./pages/Sellcar";
import Profile from "./pages/Profile";
import EditProfile from "./pages/EditProfile";
import SuccessSold from "./pages/SuccessSold";
import Sold from "./pages/Sold";
import { SoldProvider } from "./context/SoldContext";
function App() {
return (
<>
<Navbar />
<SoldProvider>
<Routes>
{/* Public Routes */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/cars" element={<Inventory />} />
<Route path="/cars/:userId/:carId" element={<CarDetails />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/SuccessSold" element={<SuccessSold />} />
{/* Protected Routes */}
<Route path="/profile" element={<Profile />} />
<Route path="/profile/edit" element={<EditProfile />} />
<Route path="/cart" element={<Cart />} />
<Route path="/orders" element={<MyOrders />}/>
<Route path="/checkout" element={<Checkout />}/>
<Route path="/edit-car/:userId/:carId" element={<SellYourCar />}/>
<Route path="/sellcar" element={<SellYourCar />}/>
<Route path="/sold" element={<Sold />}/>
<Route path="/wishlist" element={<Wishlist />}/>
<Route path="/receipt" element={<ReceiptPage />}/>
<Route path="/success" element={<Success />}/>
</Routes>
</SoldProvider>
<Footer />
</>
);
}
export default App;
import { useLayoutEffect, useRef } from "react";
import { useLocation, useNavigationType } from "react-router-dom";
function getScroller() {
return document.scrollingElement || document.documentElement;
}
export default function SmartScroll({ ready = true }) {
const location = useLocation();
const navType = useNavigationType();
const restored = useRef(false);
useLayoutEffect(() => {
if ("scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual";
}
const scroller = getScroller();
const key = `scroll:${location.key}`;
const restoreScroll = () => {
const saved = sessionStorage.getItem(key);
if (saved) {
const { x = 0, y = 0 } = JSON.parse(saved);
scroller.scrollTo({ left: x, top: y, behavior: "auto" });
restored.current = true;
}
};
if (navType === "POP") {
// Wait until content is ready
if (ready) {
restoreScroll();
} else {
const interval = setInterval(() => {
if (ready && !restored.current) {
restoreScroll();
clearInterval(interval);
}
}, 50);
return () => clearInterval(interval);
}
} else {
// New page → scroll to top
scroller.scrollTo({ left: 0, top: 0, behavior: "auto" });
}
// Save scroll on unmount
return () => {
const s = getScroller();
sessionStorage.setItem(key, JSON.stringify({ x: s.scrollLeft, y: s.scrollTop }));
};
}, [location, navType, ready]);
return null;
}
import { useEffect, useState, useLayoutEffect, useRef } from "react";
import { collectionGroup, getDocs, query } from "firebase/firestore";
import { db } from "../../firebase/firebase";
import CarCard from "../../components/cars/CarCard";
import Cambodia from "../../assets/images/logo/Cambodia.png";
export default function CarList({ filters = {}, sortOption }) {
const [cars, setCars] = useState([]);
const [carsToDisplay, setCarsToDisplay] = useState(12);
const containerRef = useRef(null); // New: ref for car grid container
// Fetch cars
useEffect(() => {
const fetchCars = async () => {
const savedCars = sessionStorage.getItem("carsList");
if (savedCars) {
setCars(JSON.parse(savedCars));
return;
}
const carsCollection = collectionGroup(db, "cars");
const q = query(carsCollection);
const querySnapshot = await getDocs(q);
const fetchedCars = querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
const shuffledCars = fetchedCars.sort(() => Math.random() - 0.5);
setCars(shuffledCars);
sessionStorage.setItem("carsList", JSON.stringify(shuffledCars));
};
fetchCars();
}, []);
const filterAndSortCars = () => {
let result = [...cars];
const { brand, condition, location, price, search, type, year } = filters;
result = result.filter((car) => {
const matchesSearch =
!search || `${car.name} ${car.model}`.toLowerCase().includes(search.toLowerCase());
const matchesLocation = location === "All locations" || car.location === location;
const matchesBrand = brand === "All brands" || car.name === brand;
const matchesType = type === "All types" || car.type === type;
const matchesCondition = condition === "All conditions" || car.condition === condition;
const matchesYear = year === "All years" || car.year === year;
const matchesPrice =
price === "No max" || (price && car.price <= parseInt(price.replace(/D/g, ""), 10));
return (
matchesSearch &&
matchesLocation &&
matchesBrand &&
matchesType &&
matchesCondition &&
matchesYear &&
matchesPrice
);
});
switch (sortOption) {
case "price-asc":
return result.sort((a, b) => a.price - b.price);
case "price-desc":
return result.sort((a, b) => b.price - a.price);
case "year-desc":
return result.sort((a, b) => b.year - a.year);
case "year-asc":
return result.sort((a, b) => a.year - b.year);
default:
return result;
}
};
const filteredCars = filterAndSortCars();
const carsToShow = filteredCars.slice(0, carsToDisplay);
const handleViewMore = () => setCarsToDisplay((prev) => prev + 8);
// ✅ Scroll restoration
useLayoutEffect(() => {
const key = "cars-scroll"; // fixed key for CarList
const scroller = document.scrollingElement || document.documentElement;
const restoreScroll = () => {
const saved = sessionStorage.getItem(key);
if (saved) {
const { x = 0, y = 0 } = JSON.parse(saved);
scroller.scrollTo({ left: x, top: y, behavior: "auto" });
}
};
// Only restore when grid has been painted
if (cars.length > 0) {
restoreScroll();
}
return () => {
sessionStorage.setItem(
key,
JSON.stringify({ x: scroller.scrollLeft, y: scroller.scrollTop })
);
};
}, [cars]);
return (
<section className="m-6 mx-2 rounded-[2px] bg-card p-3 px-2 md:m-4 md:p-3 sm:px-10 lg:m-6 lg:mx-10 lg:p-4" ref={containerRef}>
<div className="mx-auto max-w-7xl">
<div className="mb-5 flex items-center gap-2">
<h2 className="text-xl font-bold text-gray-800">Cars for Sale</h2>
<img src={Cambodia} alt="Cambodia Flag" className="h-7 w-10" />
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{carsToShow.length > 0
? carsToShow.map((car) => <CarCard key={car.id} car={car} />)
: (
<div className="col-span-full py-10 text-center text-gray-500">
<p className="text-lg">No cars found matching your criteria.</p>
<p className="mt-2 text-sm">Try adjusting your filters or search query.</p>
</div>
)}
</div>
{filteredCars.length > carsToDisplay && (
<div className="mt-8 text-center">
<button onClick={handleViewMore} className="font-base text-[#2384C1] underline">
View More
</button>
</div>
)}
</div>
</section>
);
}