Skip to content

Commit 5ff4f15

Browse files
committed
Document usePreventremove hook
1 parent ee3679c commit 5ff4f15

File tree

6 files changed

+415
-249
lines changed

6 files changed

+415
-249
lines changed

versioned_docs/version-7.x/custom-routers.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,4 @@ const MyStackRouter = (options) => {
214214
};
215215
```
216216
217-
If you want to prevent going back, the recommended approach is to use the [`beforeRemove` event](preventing-going-back.md).
217+
If you want to prevent going back, the recommended approach is to use the [`usePreventRemove` hook](preventing-going-back.md).

versioned_docs/version-7.x/navigation-events.md

+44-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,50 @@ This event is emitted when the navigator's state changes. This event receives th
3131

3232
### `beforeRemove`
3333

34-
This event is emitted when the user is leaving the screen, there's a chance to [prevent the user from leaving](preventing-going-back.md).
34+
This event is emitted when the user is leaving the screen due to a navigation action. It is possible to prevent the user from leaving the screen by calling `e.preventDefault()` in the event listener.
35+
36+
```js
37+
React.useEffect(
38+
() =>
39+
navigation.addListener('beforeRemove', (e) => {
40+
if (!hasUnsavedChanges) {
41+
return;
42+
}
43+
44+
// Prevent default behavior of leaving the screen
45+
e.preventDefault();
46+
47+
// Prompt the user before leaving the screen
48+
Alert.alert(
49+
'Discard changes?',
50+
'You have unsaved changes. Are you sure to discard them and leave the screen?',
51+
[
52+
{
53+
text: "Don't leave",
54+
style: 'cancel',
55+
onPress: () => {
56+
// Do nothing
57+
},
58+
},
59+
{
60+
text: 'Discard',
61+
style: 'destructive',
62+
// If the user confirmed, then we dispatch the action we blocked earlier
63+
// This will continue the action that had triggered the removal of the screen
64+
onPress: () => navigation.dispatch(e.data.action),
65+
},
66+
]
67+
);
68+
}),
69+
[navigation, hasUnsavedChanges]
70+
);
71+
```
72+
73+
:::note
74+
75+
Preventing the action in this event doesn't work properly with [`@react-navigation/native-stack`](native-stack-navigator.md). We recommend using the [`usePreventRemove` hook](preventing-going-back.md) instead.
76+
77+
:::
3578

3679
## Listening to events
3780

versioned_docs/version-7.x/preventing-going-back.md

+66-246
Original file line numberDiff line numberDiff line change
@@ -7,270 +7,90 @@ sidebar_label: Preventing going back
77
import Tabs from '@theme/Tabs';
88
import TabItem from '@theme/TabItem';
99

10-
Sometimes you may want to prevent the user from leaving a screen, for example, if there are unsaved changes, you might want to show a confirmation dialog. You can achieve it by using the `beforeRemove` event.
10+
Sometimes you may want to prevent the user from leaving a screen to avoid losing unsaved changes. There are a couple of things you may want to do in this case:
1111

12-
The event listener receives the `action` that triggered it. You can dispatch this action again after confirmation, or check the action object to determine what to do.
12+
## Prevent the user from leaving the screen
1313

14-
Example:
14+
The `usePreventRemove` hook allows you to prevent the user from leaving a screen. See the [`usePreventRemove`](use-prevent-remove.md) docs for more details.
1515

16-
<Tabs groupId="config" queryString="config">
17-
<TabItem value="static" label="Static" default>
18-
19-
```js name="Prevent going back" snack version=7
20-
import * as React from 'react';
21-
import { Button, Alert, View, TextInput, StyleSheet } from 'react-native';
22-
import {
23-
useNavigation,
24-
createStaticNavigation,
25-
} from '@react-navigation/native';
26-
import { createStackNavigator } from '@react-navigation/stack';
27-
28-
// codeblock-focus-start
29-
const EditTextScreen = () => {
30-
const [text, setText] = React.useState('');
31-
const navigation = useNavigation();
32-
33-
const hasUnsavedChanges = Boolean(text);
34-
35-
React.useEffect(
36-
() =>
37-
navigation.addListener('beforeRemove', (e) => {
38-
const action = e.data.action;
39-
if (!hasUnsavedChanges) {
40-
return;
41-
}
42-
43-
e.preventDefault();
44-
45-
Alert.alert(
46-
'Discard changes?',
47-
'You have unsaved changes. Are you sure to discard them and leave the screen?',
48-
[
49-
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
50-
{
51-
text: 'Discard',
52-
style: 'destructive',
53-
onPress: () => navigation.dispatch(e.data.action),
54-
},
55-
]
56-
);
57-
}),
58-
[hasUnsavedChanges, navigation]
59-
);
60-
61-
return (
62-
<View style={styles.content}>
63-
<TextInput
64-
autoFocus
65-
style={styles.input}
66-
value={text}
67-
placeholder="Type something…"
68-
onChangeText={setText}
69-
/>
70-
</View>
71-
);
72-
};
73-
// codeblock-focus-end
74-
75-
const HomeScreen = () => {
76-
const navigation = useNavigation();
77-
78-
return (
79-
<View style={styles.buttons}>
80-
<Button
81-
title={'Push EditText'}
82-
onPress={() => navigation.push('EditText')}
83-
style={styles.button}
84-
/>
85-
</View>
86-
);
87-
};
88-
89-
const RootStack = createStackNavigator({
90-
screens: {
91-
Home: HomeScreen,
92-
EditText: EditTextScreen,
93-
},
94-
});
95-
96-
const Navigation = createStaticNavigation(RootStack);
97-
98-
export default function App() {
99-
return <Navigation />;
100-
}
101-
102-
const styles = StyleSheet.create({
103-
content: {
104-
flex: 1,
105-
padding: 16,
106-
},
107-
input: {
108-
margin: 8,
109-
padding: 10,
110-
borderRadius: 3,
111-
borderWidth: StyleSheet.hairlineWidth,
112-
borderColor: 'rgba(0, 0, 0, 0.08)',
113-
backgroundColor: 'white',
114-
},
115-
buttons: {
116-
flex: 1,
117-
justifyContent: 'center',
118-
padding: 8,
119-
},
120-
button: {
121-
margin: 8,
122-
},
123-
});
124-
```
125-
126-
</TabItem>
127-
<TabItem value="dynamic" label="Dynamic">
128-
129-
```js name="Prevent going back" snack version=7
130-
import * as React from 'react';
131-
import { Button, Alert, View, TextInput, StyleSheet } from 'react-native';
132-
import { NavigationContainer, useNavigation } from '@react-navigation/native';
133-
import { createStackNavigator } from '@react-navigation/stack';
134-
135-
// codeblock-focus-start
136-
const EditTextScreen = () => {
137-
const navigation = useNavigation();
138-
const [text, setText] = React.useState('');
139-
140-
const hasUnsavedChanges = Boolean(text);
141-
142-
React.useEffect(
143-
() =>
144-
navigation.addListener('beforeRemove', (e) => {
145-
const action = e.data.action;
146-
if (!hasUnsavedChanges) {
147-
return;
148-
}
149-
150-
e.preventDefault();
151-
152-
Alert.alert(
153-
'Discard changes?',
154-
'You have unsaved changes. Are you sure to discard them and leave the screen?',
155-
[
156-
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
157-
{
158-
text: 'Discard',
159-
style: 'destructive',
160-
onPress: () => navigation.dispatch(e.data.action),
161-
},
162-
]
163-
);
164-
}),
165-
[hasUnsavedChanges, navigation]
166-
);
167-
168-
return (
169-
<View style={styles.content}>
170-
<TextInput
171-
autoFocus
172-
style={styles.input}
173-
value={text}
174-
placeholder="Type something…"
175-
onChangeText={setText}
176-
/>
177-
</View>
178-
);
179-
};
180-
// codeblock-focus-end
181-
182-
const HomeScreen = () => {
183-
const navigation = useNavigation();
184-
185-
return (
186-
<View style={styles.buttons}>
187-
<Button
188-
title={'Push EditText'}
189-
onPress={() => navigation.push('EditText')}
190-
style={styles.button}
191-
/>
192-
</View>
193-
);
194-
};
195-
196-
const Stack = createStackNavigator();
197-
198-
export default function App() {
199-
return (
200-
<NavigationContainer>
201-
<Stack.Navigator>
202-
<Stack.Screen name="Home" component={HomeScreen} />
203-
<Stack.Screen name="EditText" component={EditTextScreen} />
204-
</Stack.Navigator>
205-
</NavigationContainer>
206-
);
207-
}
208-
209-
const styles = StyleSheet.create({
210-
content: {
211-
flex: 1,
212-
padding: 16,
213-
},
214-
input: {
215-
margin: 8,
216-
padding: 10,
217-
borderRadius: 3,
218-
borderWidth: StyleSheet.hairlineWidth,
219-
borderColor: 'rgba(0, 0, 0, 0.08)',
220-
backgroundColor: 'white',
221-
},
222-
buttons: {
223-
flex: 1,
224-
justifyContent: 'center',
225-
padding: 8,
226-
},
227-
button: {
228-
margin: 8,
229-
},
230-
});
231-
```
232-
233-
</TabItem>
234-
</Tabs>
235-
236-
<video playsInline autoPlay muted loop>
237-
<source src="/assets/behavior/prevent-closing.mp4" />
238-
</video>
16+
<details>
17+
<summary>Previous approach</summary>
23918

24019
Previously, the way to do this was to:
24120

242-
- Override back button in header
21+
- Override the back button in the header
24322
- Disable back swipe gesture
24423
- Override system back button/gesture on Android
24524

246-
However, this approach has many important differences in addition to being less code:
25+
However, using the hook has many important differences in addition to being less code:
24726

24827
- It's not coupled to any specific buttons, going back from custom buttons will trigger it as well
249-
- It's not coupled to any specific actions, any action that removes the route from state will trigger it
250-
- It works across nested navigators, e.g. if the screen is being removed due to an action in parent navigator
251-
- User can still swipe back in the stack navigator, however, the swipe will be cancelled if the event was prevented
28+
- It's not coupled to any specific actions, any action that removes the route from the state will trigger it
29+
- It works across nested navigators, e.g. if the screen is being removed due to an action in the parent navigator
30+
- The user can still swipe back in the stack navigator, however, the swipe will be canceled if the event is prevented
25231
- It's possible to continue the same action that triggered the event
25332

254-
## Limitations
33+
</details>
34+
35+
## Prevent the user from leaving the app
36+
37+
To be able to prompt the user before they leave the app on Android, you can use the `BackHandler` API from React Native:
38+
39+
```js
40+
import { Alert, BackHandler } from 'react-native';
41+
42+
// ...
43+
44+
React.useEffect(() => {
45+
const onBackPress = () => {
46+
Alert.alert(
47+
'Exit App',
48+
'Do you want to exit?',
49+
[
50+
{
51+
text: 'Cancel',
52+
onPress: () => {
53+
// Do nothing
54+
},
55+
style: 'cancel',
56+
},
57+
{ text: 'YES', onPress: () => BackHandler.exitApp() },
58+
],
59+
{ cancelable: false }
60+
);
61+
62+
return true;
63+
};
64+
65+
const backHandler = BackHandler.addEventListener(
66+
'hardwareBackPress',
67+
onBackPress
68+
);
25569

256-
There are couple of limitations to be aware of when using the `beforeRemove` event. The event is **only** triggered whenever a screen is being removed due to a navigation state change. For example:
70+
return () => backHandler.remove();
71+
}, []);
72+
```
25773

258-
- The user pressed back button on a screen in a stack.
259-
- The user performed a swipe back gesture.
260-
- Some action such as `pop` or `reset` was dispatched which removes the screen from the state.
74+
On the Web, you can use the `beforeunload` event to prompt the user before they leave the browser tab:
26175

262-
This event is **not** triggered when a screen is being unfocused but not removed. For example:
76+
```js
77+
React.useEffect(() => {
78+
const onBeforeUnload = (event) => {
79+
// Prevent the user from leaving the page
80+
event.preventDefault();
81+
event.returnValue = true;
82+
};
26383

264-
- The user pushed a new screen on top of the screen with the listener in a stack.
265-
- The user navigated from one tab/drawer screen to another tab/drawer screen.
84+
window.addEventListener('beforeunload', onBeforeUnload);
26685

267-
The event is also **not** triggered when the user is exiting the screen due to actions not controlled by the navigation state:
86+
return () => {
87+
window.removeEventListener('beforeunload', onBeforeUnload);
88+
};
89+
}, []);
90+
```
26891

269-
- The user closes the app (e.g. by pressing the back button on the home screen, closing the tab in the browser, closing it from app switcher etc.). You can additionally use [`hardwareBackPress`](https://reactnative.dev/docs/backhandler) event on Android, [`beforeunload`](https://developer.mozilla.org/en-US/docs/web/api/window/beforeunload_event) event on Web etc. to handle some of these cases.
270-
- A screen gets unmounted due to conditional rendering, or due to a parent component being unmounted.
271-
- A screen gets unmounted due to usage of `unmountOnBlur` options with [`@react-navigation/bottom-tabs`](bottom-tab-navigator.md), [`@react-navigation/drawer`](drawer-navigator.md) etc.
92+
:::warning
27293

273-
In addition to the above scenarios, this feature also doesn't work properly with [`@react-navigation/native-stack`](native-stack-navigator.md). To make this work, you need to:
94+
The user can still close the app by swiping it away from the app switcher or closing the browser tab. Or the app can be closed by the system due to low memory or other reasons. It's also not possible to prevent leaving the app on iOS. We recommend persisting the data and restoring it when the app is opened again instead of prompting the user before they leave the app.
27495

275-
- Disable the swipe gesture for the screen (`gestureEnabled: false`).
276-
- Override the native back button in the header with a custom back button (`headerLeft: (props) => <CustomBackButton {...props} />`).
96+
:::

0 commit comments

Comments
 (0)