Skip to Content

Navigation

Learn how navigation works in AppShell.

With AppShell’s navigation helpers, micro frontend applications can trigger a redirect navigation to the main application it resides in. To achieve this, you’ll need to implement the necessary methods and pass it to the AppShellProvider component.

It’s not important which routing library you use to integrate navigation capabilities in your application. You just need to implement given navigation helper methods with your application logic.

Overview

The AppShell manages navigation through the ShellNavigation interface, which defines how navigation actions are handled across the application. This approach ensures consistency in navigation behavior and allows for optimizations such as preloading and lazy loading of resources.

This setup allows micro frontends to request navigation through the AppShell, maintaining loose coupling between components.

import { AppShellProvider, type ShellNavigation } from '@akinon/app-shell'; const App = () => { const navigation: ShellNavigation = { navigate: ({ id, path, external, newTab }) => { // Implement the navigation logic here } }; return ( <AppShellProvider apps={...} data={...} navigation={navigation} actions={...} > {/* Your application components go here */} </AppShellProvider> ); }; export default App;

Type

/** * Provides a mechanism for navigating within the shell application. * * @typedef {Object} ShellNavigation * @property {(payload: ShellNavigationPayload) => void} navigate - A function that performs navigation to the specified URL. * @property {(payload: ShellNavigationPayload) => string | undefined} resolveUrl - Optional. Returns the absolute url the host would navigate to for the given payload, without navigating, or nothing when it has none. */ interface ShellNavigation { navigate: (payload: ShellNavigationPayload) => void; resolveUrl?: (payload: ShellNavigationPayload) => string | undefined; } interface ShellNavigationPayload { id?: number | UUID; path: string; external?: boolean; newTab?: boolean; }

With this implementation, micro applications will have the ability to redirect main application to desired path.

Opening a navigation in a new browser tab

A micro frontend can ask for a destination to be opened in a new browser tab by passing newTab: true to its own navigate call. The AppShell forwards the flag to your navigate implementation untouched and never opens a window itself: only the host knows its own url grammar, and a tab opened from inside the iframe would resolve the path against the iframe’s origin instead of yours.

Build the url with the same helper the same-tab branch uses, so the two can never drift apart, and make it absolute before handing it to window.open. An external path arrives from the iframe untouched, so check its scheme first: only an http: or https: url may reach window.open, never a javascript: or data: one, which would run in your page’s context.

const buildUri = ({ id, path, external }: ShellNavigationPayload) => external ? path : `/apps/${resolveSlug(id)}${path || ''}`; const navigation: ShellNavigation = { navigate: payload => { const uri = buildUri(payload); if (payload.newTab) { // Absolute, so the new tab never resolves against the iframe's own url. const target = new URL(uri, window.location.origin); // An external path arrives from the iframe untouched: open web urls only, // never a javascript: or data: one. if (target.protocol !== 'http:' && target.protocol !== 'https:') return; window.open(target.href, '_blank', 'noopener,noreferrer'); return; } history.push(uri); } };

A new tab leaves the current page where it is, so a navigation requested with newTab: true from inside a rich modal does not close that modal. Without the flag, the modal is closed and its parent client is navigated, as before.

Resolving urls without navigating

Opening a new tab programmatically is not always enough: an extension that wants a real <a href target="_blank"> — middle-clickable, copyable, shown in the status bar — has to know the url up front. Implement the optional resolveUrl callback for that. It answers what url would you navigate to for this payload without performing the navigation.

const navigation: ShellNavigation = { navigate: payload => { // ... }, resolveUrl: payload => new URL(buildUri(payload), window.location.origin).href };

The AppShell broadcasts the returned url to the micro frontend as data.shellBaseUrl, and the client joins its own paths onto it through resolveShellUrl. Three things are worth knowing about how it is called:

  • It is called on every SET_DATA emit — once per app when that app’s iframe hands over its config, once per rich-modal iframe, and again for every app whenever the data prop you pass to AppShellProvider changes. Keep it cheap and free of side effects.
  • The path is always empty, so what you return carries your url grammar only. The application’s own base path is applied by the client.
  • The id belongs to the app that owns the bus. A rich modal is resolved with its parent app’s id, because the modal’s uuid is not an app you ever registered.

Return an absolute url. The AppShell drops any query string and hash from it before it is broadcast — the client appends the app path to it as plain text, so either would swallow the path. A relative url is resolved against the shell’s own origin. If resolveUrl returns undefined or an empty string, throws, or is not implemented at all, shellBaseUrl is simply left out of the data the app receives — the AppShell never substitutes the page origin, because a url built from the wrong origin is the failure this callback exists to prevent. Clients handle its absence by falling back to navigate({ path, newTab: true }).

Taking this version as a host

Two obligations come with it, and a host that skips them degrades quietly rather than loudly:

  • Honour newTab in your navigate. A host that ignores the flag navigates in the same tab, so a client’s CTA silently loses the user’s place instead of opening beside it.
  • Open the window synchronously. navigate runs inside a postMessage handler; a browser allows window.open there only while the user’s gesture is still live. Do the window.open in that call, not after an await, or the popup blocker swallows it and there is nothing to catch.
  • Open web urls only. An external path is whatever the iframe sent. Check that the url you are about to open is http: or https: before calling window.open, as in the example above; the AppShell does not filter it.

Implementing resolveUrl is optional and independent: without it the programmatic path still works and only the declarative <a href> is unavailable.