Skip to content

Add useBackButton hook - #60

Open
yuenler wants to merge 3 commits into
mainfrom
back-button
Open

Add useBackButton hook#60
yuenler wants to merge 3 commits into
mainfrom
back-button

Conversation

@yuenler

@yuenler yuenler commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a useBackButton hook so the browser's back button navigates within the app instead of exiting it.

How it's used

Call it once at the top level with a handler that returns home, then use the exported backButtonController to mark when a subpanel is entered and to declare its state:

useBackButton(() => dispatch({ type: ShowHome }));

backButtonController.onSubpanelEntered();
backButtonController.setSubpanelState(BackState.UnsavedChanges);

The library handles the rest — back goes straight home when Normal, asks for confirmation on UnsavedChanges, and explains why it can't leave when Blocked. It uses reactkit's existing alert/confirm, so no new dependencies.

Notes

Exports useBackButton, backButtonController, and the BackState / BackButtonController types. Controller state is module-level so subpanels don't need prop drilling. Assumes a single level of navigation.

The fiddly parts are handled internally: popstate can't be canceled, so blocking works by synchronously pushing a replacement entry before any dialog is awaited — and consuming the history entry is asymmetric depending on whether the browser back button or an in-app button triggered it.

@yuenler yuenler changed the title Add useBrowserBackButton hook Add useBackButton hook Aug 10, 2026

@gabeabrams gabeabrams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great structure! See comments

/*------------------------------------------------------------------------*/

// Marker stored on history entries owned by this hook
const HISTORY_STATE_MARKER = { dceReactkitBackButton: true };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Capitalize "Kit"


// Default message shown when the user tries to go back while blocked
const BLOCKED_TITLE = 'Cannot Go Back';
const BLOCKED_MESSAGE = 'We are currently working. Please try again in a moment.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We're trying to move away from using "we" because it's ambiguous. How about:
"An active task is in progress. Please try again later."

Can you further improve upon this language?


// Default message shown when the user tries to go back with unsaved changes
const UNSAVED_CHANGES_TITLE = 'Abandon Changes?';
const UNSAVED_CHANGES_MESSAGE = 'Your current progress will be lost.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about "Any unsaved changes may be lost."


// True while a confirmation/alert is on screen, so repeated back presses don't
// stack up duplicate prompts
let promptVisible = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So many variables. Are they interdependent? If so, maybe we make a "BackButtonState" type and store it as one variable with rules about its structure?

customBlockedMessage = undefined;

// Update the app
handleGoHome?.();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we need to throw an error if handleGoHome isn't defined right?

* Undo a back navigation that we don't want to allow, keeping the user in
* place. The browser's popstate event cannot be canceled, so we immediately
* push a replacement entry instead. Must be called synchronously while
* handling the pop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fascinating hack that might get us in trouble with the browser? haha >.< But interesting

* @author Yuen Ler Chow
*/
export const backButtonController: BackButtonController = {
onSubpanelEntered: () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

JSDoc for each function


// Check whether the user is allowed to leave right now
if (!force) {
if (promptVisible) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Explain this logic via comment

// Keep the handler up to date so the listener never calls a stale version
handleGoHome = handleGoHomeFunc;

useEffect(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Explain this use of useEffect and why we need it

Comment thread src/types/BackState.ts
*/
enum BackState {
// The user can go back immediately, no confirmation required
Normal = 'normal',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use our typical formatting:
Normal = 'Normal',

* fix gaps between items

* remove unnnecessary comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants