Skip to content

Instantly share code, notes, and snippets.

@syntaxhacker
Created April 21, 2026 06:02
Show Gist options
  • Select an option

  • Save syntaxhacker/46f779214cdc840c51a2ef8a9b9c76dd to your computer and use it in GitHub Desktop.

Select an option

Save syntaxhacker/46f779214cdc840c51a2ef8a9b9c76dd to your computer and use it in GitHub Desktop.
Generic React Router hook for stack-based navigation. Preserves query params on back, prevents history accumulation, deterministic routing.
/**
* useNavigationStack - Generic React Router navigation stack hook
*
* Usage:
* const nav = useNavigationStack('/items'); // fallback route
*
* // Forward navigation - pushes current path to stack
* <button onClick={() => nav.goTo('/items/123')}>View Details</button>
* <button onClick={() => nav.goTo('/items/edit/123', { replace: true })}>Edit</button>
*
* // Back navigation - pops from stack
* <button onClick={nav.goBack}>Back</button>
*
* // Go back to a specific route type (finds it in stack)
* <button onClick={() => nav.goBackTo('/items')}>Back to List</button>
*/
import { useCallback, useRef } from "react";
import { useNavigate, useLocation } from "react-router-dom";
const useNavigationStack = (defaultBackRoute = "/") => {
const navigate = useNavigate();
const location = useLocation();
const stackRef = useRef([]);
const currentFullPath = location.pathname + location.search;
const push = useCallback(
(path) => {
stackRef.current = [...stackRef.current, path ?? currentFullPath].slice(-20);
},
[currentFullPath]
);
const pop = useCallback(() => {
const stack = stackRef.current;
const top = stack[stack.length - 1] ?? defaultBackRoute;
stackRef.current = stack.slice(0, -1);
return top;
}, [defaultBackRoute]);
const peek = useCallback(() => {
const stack = stackRef.current;
return stack[stack.length - 1] ?? null;
}, []);
const clear = useCallback(() => {
stackRef.current = [];
}, []);
const goTo = useCallback(
(path, options = {}) => {
const { push: shouldPush = true, replace = false, state } = options;
if (shouldPush) {
push(currentFullPath);
}
navigate(path, { replace, state });
},
[navigate, push, currentFullPath]
);
const goBack = useCallback(
(fallback) => {
const destination = pop();
navigate(fallback ?? destination, { replace: true });
},
[navigate, pop]
);
const goBackTo = useCallback(
(targetPath) => {
const stack = stackRef.current;
const matchIndex = stack.findIndex((entry) => {
try {
return new URL(entry, window.location.origin).pathname === targetPath;
} catch {
return entry.startsWith(targetPath);
}
});
if (matchIndex !== -1) {
const destination = stack[matchIndex];
stackRef.current = stack.slice(0, matchIndex);
navigate(destination, { replace: true });
} else {
navigate(pop(), { replace: true });
}
},
[navigate, pop]
);
const depth = stackRef.current.length;
return { goTo, goBack, goBackTo, push, pop, peek, clear, depth, currentFullPath };
};
export default useNavigationStack;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment