Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions app/hooks/use-custom-back-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useEffect } from 'react';
import { useNavigationHistory } from './use-navigation-history';

export const useCustomBackButton = () => {
const { pop, canGoBack } = useNavigationHistory();

useEffect(() => {
const handlePopState = (event: PopStateEvent) => {
// We need to prevent the default browser back navigation
// and use our custom `pop` function if we can go back.
// The `PopStateEvent` is triggered by browser's back/forward buttons.
// Our `useNavigationHistory`'s useEffect already tries to sync the historyStack
// if `location.state.historyStack` is present.
// The main job here is to ensure that if `canGoBack` is true,
// we prefer our `pop` logic.

// event.preventDefault(); // This is not directly possible in onpopstate in a reliable way.
// Instead, the logic in useNavigationHistory's useEffect for location changes
// should correctly interpret the state and adjust the historyStack.
// What we want to ensure is that a "browser back" action correctly
// triggers our `pop` if the previous state in our stack is the target.

// The current `useNavigationHistory` `useEffect` should handle synchronization.
// Let's test if simply relying on that is enough.
// If not, we might need to explicitly call `pop()` here under certain conditions,
// for example, if the new location (after browser back) matches what `pop()` would navigate to.

// For now, this effect will primarily be a placeholder to remind us
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested this? Seems like this lots of comments is saying it might now work as implemented

// that this is where explicit back button handling logic would go if needed.
// The actual stack management on popstate is currently handled by the
// useEffect in useNavigationHistory.

// Let's add a log to see when this is triggered.
console.log('PopStateEvent triggered', event.state, window.location.pathname);

// One potential strategy:
// If `event.state` does NOT contain our `historyStack`, it means it's a navigation
// outside our custom history system's full control (e.g. very first page, or user manually changed URL).
// In this case, `useNavigationHistory`'s `useEffect` should reset the stack.
// If `event.state` DOES contain `historyStack`, `useNavigationHistory`'s `useEffect`
// should already sync to it. Our `pop` function also updates the state with the new stack.
// The key is that when the user clicks "back", the browser goes to the *previous state object*.
// If that previous state object was set by our `push` or `pop` method, it contains the
// correct `historyStack` for that point in time.
};

window.addEventListener('popstate', handlePopState);

return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [pop, canGoBack]); // Dependencies for the effect

// Potentially, we can expose a function to be called by a UI back button.
const goBack = () => {
if (canGoBack) {
pop();
} else {
// Optional: Handle case where there's no place to go back in the custom stack
// e.g., navigate to a default page or exit app (if in a PWA context)
console.log("Cannot go back further in custom history.");
}
};

return { goBack, canGoBack };
};
118 changes: 118 additions & 0 deletions app/hooks/use-navigation-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { To, useLocation, useNavigate } from 'react-router';
import { useState, useEffect, useCallback } from 'react'; // Added useCallback

export type NavigationEntry = {
pathname: string;
search: string;
hash: string;
state?: any;
};

// Updated getNavigationEntry to be more flexible
const locationToEntry = (
location: Pick<ReturnType<typeof useLocation>, 'pathname' | 'search' | 'hash'>,
state?: any,
): NavigationEntry => ({
pathname: location.pathname,
search: location.search,
hash: location.hash,
state: state ?? {}, // Ensure state is always an object
});

const toToEntry = (
to: To,
currentState?: any, // Current state of the page we are on, if needed
navOptionsState?: any // State provided in navigate(to, {state: navOptionsState})
): NavigationEntry => {
if (typeof to === 'string') {
// Simple path string
const url = new URL(to, window.location.origin); // Use a base for proper parsing
return {
pathname: url.pathname,
search: url.search,
hash: url.hash,
state: { ...navOptionsState } // State comes from navigation options
};
}
// 'to' is a Path object
return {
pathname: to.pathname ?? '/', // Default to '/' if pathname is somehow undefined
search: to.search ?? '',
hash: to.hash ?? '',
state: { ...navOptionsState } // State comes from navigation options
};
};


const entryToTo = (entry: NavigationEntry): To => ({
pathname: entry.pathname,
search: entry.search,
hash: entry.hash,
});

export const useNavigationHistory = () => {
const location = useLocation();
const navigate = useNavigate();

const initializeStack = (): NavigationEntry[] => {
if (location.state?.historyStack && Array.isArray(location.state.historyStack)) {
return location.state.historyStack;
}
return [locationToEntry(location, location.state)];
};

const [historyStack, setHistoryStack] = useState<NavigationEntry[]>(initializeStack);

const push = useCallback((to: To, options?: { replace?: boolean; state?: any }) => {
const newEntry = toToEntry(to, location.state, options?.state);
const newHistoryStack = options?.replace
? [...historyStack.slice(0, -1), newEntry]
: [...historyStack, newEntry];

const browserState = { ...newEntry.state, historyStack: newHistoryStack };

navigate(to, { replace: options?.replace, state: browserState });
setHistoryStack(newHistoryStack);
}, [navigate, historyStack, location.state]); // Added location.state to dependencies

const pop = useCallback(() => {
if (historyStack.length <= 1) {
console.log("Cannot pop the last entry from history stack.");
return;
}

const newStack = historyStack.slice(0, -1);
const targetEntry = newStack[newStack.length - 1];

// The state for the browser should be the state of the targetEntry, plus the new (shorter) history stack
const browserState = { ...targetEntry.state, historyStack: newStack };

navigate(entryToTo(targetEntry), { replace: true, state: browserState });
setHistoryStack(newStack);
}, [navigate, historyStack]);

useEffect(() => {
// This effect syncs the component's historyStack with the one from browser's location state
// This is crucial for handling browser back/forward buttons correctly.
const browserHistoryStack = location.state?.historyStack;
if (browserHistoryStack && Array.isArray(browserHistoryStack)) {
// Only update if stacks are actually different to avoid infinite loops
if (JSON.stringify(historyStack) !== JSON.stringify(browserHistoryStack)) {
setHistoryStack(browserHistoryStack);
}
} else {
// If no historyStack in browser state, means we're at a state not managed by our hook,
// or it's the very first load. Reset our stack to the current location.
// This also handles manual URL changes.
const currentEntry = locationToEntry(location, location.state);
if (historyStack.length > 1 || JSON.stringify(historyStack[0]) !== JSON.stringify(currentEntry)) {
// Only reset if it's meaningfully different or stack is longer than it should be
setHistoryStack([currentEntry]);
}
}
}, [location, historyStack]); // Removed navigate from here, it doesn't directly influence this sync logic

const canGoBack = historyStack.length > 1;

return { historyStack, push, pop, canGoBack };
};
Loading
Loading