Skip to content

feat(react-native): add opt-in back handler API#310

Open
heecheolman wants to merge 8 commits into
mainfrom
back-press
Open

feat(react-native): add opt-in back handler API#310
heecheolman wants to merge 8 commits into
mainfrom
back-press

Conversation

@heecheolman

Copy link
Copy Markdown
Collaborator

Add useBackHandler as a new subscription-based API for handling back actions before legacy useBackEvent handlers.

useBackHandler runs handlers from the latest registration first and only prevents default back navigation when a handler explicitly returns true. Returning false or undefined allows bubbling to continue and eventually falls back to legacy useBackEvent handlers or the router default back behavior.

The existing useBackEvent API remains non-breaking: variadic add/remove behavior is preserved, all registered handlers still run, and handler return values are ignored.

import { useEffect, useState } from 'react';
import { Button, View } from 'react-native';
import { useBackHandler } from '@granite-js/react-native';

export function OverlayExample() {
  const backHandler = useBackHandler();
  const [isOverlayOpen, setIsOverlayOpen] = useState(false);

  useEffect(() => {
    const subscription = backHandler.addEventListener((event) => {
      if (isOverlayOpen) {
        setIsOverlayOpen(false);

        // Prevent default back navigation.
        return true;
      }

      // Continue bubbling. Default back navigation is not blocked.
      return false;
    });

    return () => {
      subscription.remove();
    };
  }, [backHandler, isOverlayOpen]);

  return (
    <View>
      <Button title="Open Overlay" onPress={() => setIsOverlayOpen(true)} />
    </View>
  );
}

@changeset-bot

changeset-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c6d53cc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 28 packages
Name Type
@granite-js/react-native Patch
babel-preset-granite Patch
create-granite-app Patch
@granite-js/blur-view Patch
@granite-js/brownfield-module Patch
@granite-js/cli Patch
@granite-js/cookies Patch
@granite-js/screen Patch
@granite-js/image Patch
@granite-js/jest Patch
@granite-js/lottie Patch
@granite-js/mpack Patch
@granite-js/native Patch
@granite-js/naver-map Patch
@granite-js/plugin-core Patch
@granite-js/plugin-env Patch
@granite-js/plugin-hermes Patch
@granite-js/plugin-micro-frontend Patch
@granite-js/plugin-router Patch
@granite-js/plugin-rozenite Patch
@granite-js/plugin-sentry Patch
@granite-js/style-utils Patch
@granite-js/utils Patch
@granite-js/video Patch
@granite-js/vitest Patch
@granite-js/deployment-manager Patch
@granite-js/forge-cli Patch
@granite-js/pulumi-aws Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
granite-docs Ready Ready Preview, Comment Jun 8, 2026 6:28am

Request Review

@@ -0,0 +1,8 @@
import { vi } from 'vitest';

export const GraniteBrownfieldModule = {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

TODO: Brownfield test mock

expect(parent).toHaveBeenCalledWith({ source: 'backButton' });
});

it('allows default navigation when handlers return false or undefined', () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

default is false or undfined

expect(parent).not.toHaveBeenCalled();
});

it('continues bubbling while handlers return false', () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

bubbling test

Comment on lines +90 to +94
const [hasBackHandler, setHasBackHandler] = useState(false);

const syncBackEventState = useCallback(() => {
setHasBackEvent(handlersRef.size > 0);
}, [handlersRef]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

using useMemo ..?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

If we change handlersRef.size > 0 to useMemo, hasBackEvent cannot be detected correctly because changes to ref.size do not trigger a render. We need to keep useCallback and explicitly update the state.

Comment thread packages/react-native/vitest.config.mts Outdated
Comment on lines +5 to +10
resolve: {
alias: {
'@granite-js/brownfield-module': new URL('./test/brownfieldModuleMock.ts', import.meta.url).pathname,
'react-native': new URL('./test/reactNativeMock.ts', import.meta.url).pathname,
},
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do not using resolve.

we are using @granite-js/vitest plugin

Comment on lines +69 to +89
const removeHandler = useCallback(
(...handlers: Array<BackHandlerCallback>) => {
for (const handler of handlers) {
handlersRef.delete(handler);
contextRemoveBackHandler(handler);
}
},
[contextRemoveBackHandler, handlersRef]
);

const addEventListener = useCallback(
(handler: BackHandlerCallback) => {
handlersRef.add(handler);
contextAddBackHandler(handler);

return {
remove: () => removeHandler(handler),
};
},
[contextAddBackHandler, handlersRef, removeHandler]
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ref exclude effect deps

initialProps: InitialProps;
initialScheme: string;
setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => Promise<void> | void;
setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

how about oniOSBackPressHandler ?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants