|
/** |
|
* 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; |