Skip to content

Instantly share code, notes, and snippets.

@moritzsalla
Created April 5, 2024 14:48
Show Gist options
  • Save moritzsalla/a9f58f7d4b1dd88bbb29d792bcba1257 to your computer and use it in GitHub Desktop.
Save moritzsalla/a9f58f7d4b1dd88bbb29d792bcba1257 to your computer and use it in GitHub Desktop.
Simple history observer that listens to pathname & popstate events
"use client";
import { createContext, useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
const MAX_HISTORY_LENGTH = 10;
type HistoryObserver = {
history: Array<string>;
referrer?: string;
};
export const HistoryObserverContext = createContext<
HistoryObserver | undefined
>(undefined);
HistoryObserverContext.displayName = "HistoryObserverContext";
/**
* Provides a context to track the history of the router.
*
* Listens to path changes and popstate events to sync with browser history.
*
* Use `useHistoryObserver` to access the history and referrer.
*
* ```tsx
* const MyApp = () => {
* return (
* <HistoryObserverProvider>
* <MyComponent />
* </HistoryObserverProvider>
* );
* }
*
* const MyComponent = () => {
* const { history, referrer } = useHistoryObserver();
* return <div>{referrer ?? "No referrer"}</div>;
* }
* ```
*/
const HistoryObserverProvider = ({ children }: React.PropsWithChildren) => {
const isPopState = useRef(false); // Track if navigation is from a popstate event
const pathname = usePathname();
const [history, setHistory] = useState<HistoryObserver["history"]>([]);
// Listen for browser navigation events (back/forward)
// to adjust custom history
useEffect(() => {
const handlePopState = () => {
isPopState.current = true;
setHistory((currentHistory) => {
// Ignore repeated popstate events
if (currentHistory[currentHistory.length - 1] === pathname)
return currentHistory;
// Maintain sync with browser history
const nextHistory = currentHistory.slice(
0,
currentHistory.lastIndexOf(pathname) + 1,
);
return nextHistory.length ? nextHistory : [pathname];
});
};
window.addEventListener("popstate", handlePopState);
return () => window.removeEventListener("popstate", handlePopState);
}, [pathname]);
// - Append new pathnames to the history whenever nextjs navigates.
// - Limit the history length to avoid excessive memory usage.
useEffect(() => {
// Proceed if not a popstate navigation
if (!isPopState.current) {
// Ignore repeated pathnames
setHistory((prev) => {
if (prev[prev.length - 1] === pathname) return prev;
const next = [...prev, pathname].slice(-MAX_HISTORY_LENGTH);
return next;
});
}
// Reset flag
isPopState.current = false;
}, [pathname]);
const referrer = history.length > 1 ? history[history.length - 2] : undefined;
return (
<HistoryObserverContext.Provider value={{ history, referrer }}>
{children}
</HistoryObserverContext.Provider>
);
};
export default HistoryObserverProvider;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment