2

What I want to achieve is straightforward. I want the user to be forced to confirm exiting a tab navigator called 'checkout'.

I read on React Navigation docs about preventing going back about the 'beforeRemove' event which seems neat and the right thing to use.

The problem is that in their example they call Alert in the eventlistener whereas I want to show a custom modal with a yes and no button.

This is React Navigations example code:

function EditText({ navigation }) {
  const [text, setText] = React.useState('');
  const hasUnsavedChanges = Boolean(text);

  React.useEffect(
    () =>
      navigation.addListener('beforeRemove', (e) => {
        if (!hasUnsavedChanges) {
          // If we don't have unsaved changes, then we don't need to do anything
          return;
        }

        // Prevent default behavior of leaving the screen
        e.preventDefault();

        // Prompt the user before leaving the screen
        Alert.alert(
          'Discard changes?',
          'You have unsaved changes. Are you sure to discard them and leave the screen?',
          [
            { text: "Don't leave", style: 'cancel', onPress: () => {} },
            {
              text: 'Discard',
              style: 'destructive',
              // If the user confirmed, then we dispatch the action we blocked earlier
              // This will continue the action that had triggered the removal of the screen
              onPress: () => navigation.dispatch(e.data.action),
            },
          ]
        );
      }),
    [navigation, hasUnsavedChanges]
  );

  return (
    <TextInput
      value={text}
      placeholder="Type something…"
      onChangeText={setText}
    />
  );
}

This is the code I have tried:

useEffect(() => {
    navigation.addListener('beforeRemove', e => {
      if (userConfirmedExit) {
        navigation.dispatch(e.data.action);
      } else {
        e.preventDefault();
        setShowExitModal(true);
      }
    });
  }, [navigation, userConfirmedExit]);

  const handleConfirmExit = () => {
    setUserConfirmedExit(true);
    navigation.replace('ProfileTab');
  };

  const handleDeclineExit = () => setShowExitModal(false);

I am bound to use the navigation.dispatch(e.data.action) inside the eventListener but the handleConfirmExit function must live outside of it and I just can't figure out how to use the beforeRemove listener AND showing a custom modal from where I can exit the tab.

The listener is firing when pressing the back button and the modal shows but nothing happens when pressing yes (i.e running the handleConfirmExit function).

I have tried removing dependencies from the useEffect. The one thing that did work, but only on Android was this:

useEffect(() => {
    navigation.addListener('beforeRemove', e => {
      e.preventDefault();
      setShowExitModal(true);
    });
  }, [navigation]);

  const handleConfirmExit = () => {
    navigation.removeListener('beforeRemove', () => {});
    navigation.replace('ProfileTab');
  };

  const handleDeclineExit = () => setShowExitModal(false);

On iOS the modal stays onto the next screen for some reason and the culprit I think is the bad implementation of 'beforeRemove' listener in the last example.

Thank you!

Max
  • 73
  • 2
  • 9

4 Answers4

4

I have a simple solution

 navigation.addListener('beforeRemove', (e) => {
      
        if (e.data.action.type !="GO_BACK") {
           //"GO_BACK" is emitted by hardware button

          navigation.dispatch(e.data.action);
        } else {
    //your code to prevent hardware back button goes here } // 

} )

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 12 '22 at 02:26
  • What I want to achieve is to be able to wait for the user response on the , THEN do navigation.dispatch(e.data.action), IF the user pressed yes on the exit modal. The exitModal is shown on hardware back press OR if the user presses a custom back button in the header. I didn't know you could call .type like you've done in your answer (thanks for pointing it out), but can you please expand you answer to achieve what I described. You can perhaps look at my accepted answer for clarification of what I want to achieve. – Max Mar 12 '22 at 07:33
0

use BackHandler , you can use navigation.goBack() instead of BackHandler.exitApp()

import { BackHandler} from "react-native";
const backAction = () => {
  Alert.alert("Discard changes?", "Are you sure you want to exit?", [
    {
      text: "NO",
      onPress: () => null,
      style: "cancel"
    },
    { text: "YES", onPress: () => BackHandler.exitApp() }
  ]);
  return true;
};

useEffect(() => {
  BackHandler.addEventListener("hardwareBackPress", backAction);
return () => {
  BackHandler.removeEventListener("hardwareBackPress", backAction);
}   
}, []);
ViShU
  • 300
  • 2
  • 10
  • Thanks, I want the eventListener to fire when pressing a back button in the header as well, not only when pressing hardware back button. Also, I want to display a modal instead of an alert box. I got {showExitModal && } in the markup. – Max Feb 18 '22 at 13:00
0

This is what I did and it works fine, but I am sure there is a better solution out there.

const [showExitModal, setShowExitModal] = useState(false);
let exitEvent = useRef<
    EventArg<
      'beforeRemove',
      true,
      {
        action: Readonly<{
          type: string;
          payload?: object | undefined;
          source?: string | undefined;
          target?: string | undefined;
        }>;
      }
    >
  >();
  useEffect(() => {
    const unsubscribe = navigation.addListener('beforeRemove', e => {
      e.preventDefault();
      exitEvent.current = e;  
      setShowExitModal(true);    
    });
    return unsubscribe;
  }, [navigation]);

  const handleConfirmExit = () => {
    if (exitEvent.current) {
      navigation.dispatch(exitEvent.current.data.action);
    }
  };

In the markup:

 {showExitModal && (
        <CheckOutExitModal
          onYesPress={handleConfirmExit}
        />
      )}
Max
  • 73
  • 2
  • 9
0

You can store the e.data.action in a state and then use this action by calling navigation.dispath(navigationAction) like below;

const [navigationState, setNavigationState] = useState(null);

useEffect(() => {

const subscribe = navigation.addListener('beforeRemove', (e) => {

 e.preventDefault();
        setExitModal(true)
        setNavigationAction(e.data.action)

}
},[])

and then call the navigation dispatch action

Owais
  • 1
  • 1
  • Thank you @Owais, this is essentially what I did in the end. I only used a ref instead of a state to store action. See accepted answer for further reference. – Max Apr 04 '23 at 14:00