Skip to main content

Moving between screens

The moves between screens, where the commands come from, how a move finishes, how a screen gives a result back, and the defaults of every move.

SectionMoving between screensDraftDocumented, not reviewed yet: the API can change.

A list opens an item, a button opens a form over the app, a toast says that a message went out. Each is a move: a command that names a screen, and the platform shows it. A screen makes its moves with useNavigation():

src/screens/Home.tsx
import { Button } from 'react-native';
import { useNavigation } from '@apexnative/navigation';

export function Home() {
const navigation = useNavigation();
const open = () =>
navigation.push('Details', { props: { id: '42' } });
return <Button title="Open Details" onPress={open} />;
}

A tap slides Details in over Home; the back button and the swipe back return to Home, with no code. This page is the map of the moves. It answers:

  1. What moves there are, and what takes the user back from each.
  2. Where the commands come from: in a screen, and anywhere else.
  3. How the app goes back, and how it knows it can.
  4. When a move is done, and what happens when it can't be made.
  5. How a screen gives a result back.
  6. How to set a move up, once for the whole app or for one command.

Each move is shown here as it looks and works; its own page has the rest of the details.

The moves​

A move names a screen, and the platform shows it in its own way. Six kinds cover every app; pick one to see it:

The way back is the platform's: the back button and the swipe of the stack, the swipe down of a sheet, the back button of Android. They work without any code; when the app goes back itself, see Going back.

Push a screen​

The stack is the main move of an app: the list opens an item, the item opens its comments. Each screen goes over the one before, and the user goes back one by one. The screen under it stays mounted, with its state and scroll.

iOS
Android
navigation.push('Details', { props: { id: '42' } });

A stack is the root of the app, a tab, or the content of a modal. Going back to a screen further down, replacing the screens of a stack, the transitions: Stack.

A modal is a task of its own over the app: writing a message, a form, the sign-in. It has a stack of its own, so it can push its screens too, and it ends as a whole: the user finishes or cancels it, and the app is as it was.

iOS
Android
navigation.showModal('Compose', { closeButton: true });

Compose slides up over the app, with a close button that dismisses the modal. Under an iOS sheet the screen below still shows, so it doesn't disappear (see The life of a screen). The styles, the sheet with detents, a popover on iPad, a swipe that asks before closing: Modals.

Show an overlay​

An overlay shows over everything without taking the place of any screen: a toast, a banner, a call in progress. The user goes on with the screens under it.

iOS
Android
navigation.showOverlay('Toast', { props: { text: 'Saved' } });

The touches, the keyboard and an overlay that closes itself: Overlays.

Select a tab​

Tabs are the sections of the app, side by side. Each keeps its own stack, so the user leaves Inbox deep in a thread, looks at Home, and comes back to the thread. The user picks a tab; the app picks one with selectTab, by its index or by the id the layout gave it, Tab('Inbox').id('inbox').

iOS
Android
await navigation.selectTab('inbox');

The tab bar, badges, a second press on a tab, the iPad sidebar: Tabs.

Show beside a list​

On iPad, a split view shows a list and the item next to it: the settings and the section, the mail and the message. showInColumn puts a screen into the column beside the list. Where there is one column, on iPhone, in a narrow window, on an Android phone, it pushes the screen over the list instead, so the same call works everywhere.

iPadOS
navigation.showInColumn('Privacy');

Two or three columns and the sidebar: Split view.

Replace everything​

switchTo replaces the whole navigation with another root: from the sign-in to the app, and back when the user signs out. There is no way back to the old root: its screens unmount.

iOS
Android
navigation.switchTo('main');

The roots of the app and a root made on the go, such as the screens of a deep link: Switching roots. The whole sign-in flow: Sign in and sign out.

Where the commands come from​

In a screen: useNavigation​

useNavigation(), as in Home at the top of this page, gives the commands of the screen the component is in. They act from this screen: push goes into its stack, dismiss closes its modal, so they need no ids.

The same object has the commands of the whole app too: switchTo, dismissAllModals, getState. A screen needs nothing else.

Code outside the screens, such as a timer, a store or the handler of a push notification, imports navigation from the package. It has the same commands, and they act from the screen the user sees: push goes into the stack the user is looking at, a modal's if one shows. Only dismiss and setOptions are missing: they need a screen of their own.

src/notifications.ts
import { navigation } from '@apexnative/navigation';

export async function openChat(userId: string) {
await navigation.dismissAllModals();
await navigation.selectTab('inbox');
await navigation.push('Chat', { props: { userId } });
}

Once the app is launching, a command waits for the first root, so a notification that launched the app is handled after it shows. Two places are too early:

  • The top level of a module, run while the bundle loads, before the launch: the command rejects.
  • Inside willMount: an awaited command never resolves, because the first root waits for willMount. Set the context there instead, and let root pick the screen.

Which one​

useNavigation()navigation
WhereIn the components of a screen.Anywhere: stores, notifications, code outside React.
Acts fromThis screen.The screen the user sees.
Before the app startsCan't be called: there is no screen yet.Waits for the first root.

The difference shows when the user moves on before the code runs. A screen loads data and then pushes Details; meanwhile the user selected another tab. useNavigation() pushes into the stack of the screen that asked, in the tab the user left; navigation pushes where the user is now. In a screen, use useNavigation(): the move stays where it was asked for.

Code outside React that knows its screen, such as a class that a screen creates, gets the same object with screenNavigation(screen). The screen is { id, name }, as useScreen() returns it.

Going back​

The user goes back with the platform's controls. The app goes back itself with the commands of a screen, after a save for example:

CommandWhat it does
pop()Back to the screen under this one in its stack.
popTo(id)Back to a screen of this stack, by the id push resolved with.
popToRoot()Back to the first screen of this stack.
dismiss()Closes the modal or the overlay this screen is in, with all its screens.
goBack()What the back button would do here: pop() when there is a screen under this one, dismiss() when it is the first screen of a modal.

goBack() is for a screen that opens both ways, pushed in one place and in a modal in another: it doesn't need to know which.

src/screens/Compose.tsx
function Compose() {
const navigation = useNavigation();
const send = async () => {
await sendMessage(); // your own request
// pushed: back to the screen under it; in a modal: it closes
await navigation.goBack();
};
return <Button title="Send" onPress={send} />;
}

Can it go back?​

Each way back has its check. They answer at once, without await, so a screen can choose what to render with them:

Checktrue when
canPop()There is a screen under this one in its stack.
canDismiss()The screen is in a modal or an overlay.
canGoBack()goBack() has something to do: one of the two above.
{navigation.canDismiss() && (
<Button title="Close" onPress={() => navigation.dismiss()} />
)}

On navigation they answer for the screen the user sees.

Replacing a screen​

replace shows a screen in place of the whole history of the stack: it comes in as a push does, and it is alone in the stack, so there is no back. It is for a point the user shouldn't go back from, such as a payment once it is made:

// on Payment, once the payment went through
navigation.replace('OrderPlaced', { props: { orderId } });

To give the stack several screens at once, with back between them, setStack('Inbox', 'Chat') sets them: the last shows, back goes to the one before. See Stack.

When a move is done​

Every command returns a promise, and it resolves once the move is done: the animation has ended, the screen is in place, and navigation.getState() already names it. await puts moves one after another, as in openChat above: each starts when the last has ended, from the screen the last one showed.

push resolves with the id of the screen it showed; showModal and showOverlay with the id of what they showed, which dismissModal and dismissOverlay take.

A command that can't be made rejects, with the command and the screen in the error, the same on iOS and Android:

await navigation.pop(); // on the first screen of its stack:
// Error: [navigation] pop: Home is the first screen of its stack,
// there is nothing to go back to

The other cases: the screen the command acts from is gone (closed while the code waited), or a name that isn't in screens.

A result back​

A screen that picks something, such as a color, a contact, a date, gives it back through a callback in its props. Props stay in JavaScript, so a function is a prop like any other (see Props stay in JavaScript):

src/screens/ColorPicker.tsx
function ColorPicker({ onPick }: { onPick: (color: string) => void }) {
const navigation = useNavigation();
const pick = (color: string) => {
onPick(color);
navigation.pop();
};
return <Button title="Red" onPress={() => pick('red')} />;
}
// on the screen that asks; setColor is its own state
navigation.push('ColorPicker', { props: { onPick: setColor } });

A tap on Red sets the color on the screen that asked, and the user is back on it. A result that more screens need, such as the signed-in user, belongs in the app's state: a context or a store.

Setting a move up​

Each command that shows a screen takes, besides the props, the settings of this move and the options of the screen for this show:

CommandIts settingsPage
push, replacetransition: how the screen comes in.Stack, Transitions
showModalstyle, sheet, closeButton, onDismiss, transition…Modals
showOverlayblockTouches: the app under it gets no touches.Overlays
showInColumncolumn: which column of the split view.Split view
All of themoptions: the screen's options for this show, over its own.Screen options

Here Compose comes up as a sheet at half height; the user drags it to full height, and the close button dismisses it:

navigation.showModal('Compose', {
// a sheet that stops at half and at full height
sheet: { detents: ['medium', 'large'] },
closeButton: true,
});

Defaults for the whole app​

presentation of createNavigation sets every move up once, so the commands don't repeat it. A command's own settings go over it. Here every modal gets a close button:

Navigation.ts
export const Navigation = createNavigation({
// …
presentation: {
modal: { closeButton: true },
},
});

Next: screen options​

A move shows a screen with its options: the title, the buttons, the bars. Screen options shows where they come from, which win, and how a shown screen changes them.

API​