Max
Max

Reputation: 95

How to prevent going back in React Native using React Navigation and 'beforeRemove' eventListener with custom modal?

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!

Upvotes: 3

Views: 7480

Answers (4)

Owais
Owais

Reputation: 1

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

Upvotes: 0

tomsebastiantom
tomsebastiantom

Reputation: 41

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 } // 

} )

Upvotes: 4

Max
Max

Reputation: 95

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}
        />
      )}

Upvotes: 1

ViShU
ViShU

Reputation: 428

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);
}   
}, []);

Upvotes: 0

Related Questions