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: truefrom 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_DATAemit — once per app when that app’s iframe hands over its config, once per rich-modal iframe, and again for every app whenever thedataprop you pass toAppShellProviderchanges. Keep it cheap and free of side effects. - The
pathis always empty, so what you return carries your url grammar only. The application’s own base path is applied by the client. - The
idbelongs 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
newTabin yournavigate. 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.
navigateruns inside apostMessagehandler; a browser allowswindow.openthere only while the user’s gesture is still live. Do thewindow.openin that call, not after anawait, or the popup blocker swallows it and there is nothing to catch. - Open web urls only. An
externalpath is whatever the iframe sent. Check that the url you are about to open ishttp:orhttps:before callingwindow.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.