Multi root
In a scenario where you need to manage multiple parts of an application that might not always be active or visible at the same time, you can utilize Aurelia's ability to handle multiple root components. This can be particularly useful for scenarios like switching between a public-facing website and a private application after login, or for loading different parts of an application on demand.
Below is a step-by-step guide on how to create a multi-root Aurelia 2 application, complete with detailed explanations and code examples.
Strategy
The multi-root approach uses two independent Aurelia applications that can be started and stopped independently. This provides complete isolation between different parts of your application, which is beneficial for:
Complete application context separation - Each root has its own DI container, configuration, and lifecycle
Memory management - Unused application parts can be fully disposed of
Different configurations - Each root can have different plugins, services, or settings
Lazy loading of major application sections - Load heavy application parts only when needed
Legacy integration - Migrate parts of existing applications incrementally
The approach works by starting with a lightweight login application that handles authentication. Once authenticated, it stops the login application and starts the main application, ensuring clean separation of concerns and optimal resource usage.
Setting up
In the src/main.ts entry point, keep explicit references to the hosts and to each Aurelia instance. Doing so lets you dispose the login root completely before booting the authenticated root, and gives you a place to store any cross-root state the login flow collects.
// src/main.ts
import { Aurelia, StandardConfiguration, AppTask } from '@aurelia/runtime-html';
import { IEventAggregator, resolve } from '@aurelia/kernel';
import { LoginWall } from './login-wall';
import { MyApp } from './my-app';
export const AUTHENTICATED_EVENT = 'user:authenticated';
export interface AuthenticatedPayload {
username: string;
timestamp: Date;
token?: string;
}
const loginHost = document.querySelector<HTMLElement>('#login-root')!;
const appHost = document.querySelector<HTMLElement>('#main-root')!;
let loginApp: Aurelia | null = null;
let mainApp: Aurelia | null = null;
async function startLoginApp() {
loginHost.hidden = false;
loginApp = new Aurelia();
loginApp.register(
StandardConfiguration,
AppTask.hydrated(() => {
const ea = resolve(IEventAggregator);
ea.subscribeOnce<AuthenticatedPayload>(AUTHENTICATED_EVENT, async (payload) => {
loginHost.hidden = true;
await loginApp?.stop(true); // dispose the login root before booting the next one
loginApp = null;
await startMainApp(payload);
});
}),
);
loginApp.app({ host: loginHost, component: LoginWall });
await loginApp.start();
}
async function startMainApp(userData?: AuthenticatedPayload) {
appHost.hidden = false;
mainApp = new Aurelia();
mainApp.register(
StandardConfiguration,
// Add additional configurations for your main app:
// RouterConfiguration,
// ValidationConfiguration,
// etc.
);
mainApp.app({ host: appHost, component: MyApp });
await mainApp.start();
// Store reference if you need to stop this app later
// window.mainApp = mainApp;
}
startLoginApp().catch(console.error);Calling stop(true) ensures the login root is disposed before the authenticated root starts, which prevents orphaned controllers and DOM nodes. The AuthenticatedPayload interface in the snippet documents the data you plan to emit from the login wall—adjust it to match what your back end returns.
Handling Login and Root Transition
In src/login-wall.ts, we define the LoginWall class with a login method. This method will start and conduct the authentication flow and then publish the authenticated event which was subscribed to at the entry point of the application.
Updating the HTML Structure
In your index.html or equivalent, you need to have placeholders for each root component. Make sure to provide unique identifiers for each and rely on the native hidden attribute so your bootstrap code can switch visibility without adding inline styles.
Example
To try this locally or in an online sandbox:
Scaffold a fresh Aurelia 2 app (
npm create aurelia@latest).Replace the generated
main.ts,login-wall.ts, andmy-app.tswith the snippets above.Add two host elements (
<login-root>and<my-app>) toindex.htmland runnpm start.
The login root will boot first, publish AUTHENTICATED_EVENT, and then the authenticated root will take over exactly as described in this recipe.
Managing Application State
When switching roots, you need to carefully manage application state since each root has its own DI container. Here are recommended approaches:
Passing Data Between Roots
Define a typed payload and register it as an instance in the authenticated root. Registration.singleton expects a constructable type and will try to instantiate it, so use Registration.instance for plain objects.
Persistent State Options
Browser Storage - Use localStorage/sessionStorage for data that should persist
State Management Libraries - Use libraries like Aurelia Store that can work across app boundaries
Server State - Store critical state on the server and refetch as needed
URL Parameters - Pass simple state through URL parameters
Additional Considerations
Memory Management and Cleanup
When stopping an Aurelia application, most cleanup happens automatically, but be aware of:
If you no longer need the Aurelia instance itself, call au.dispose() after the stop promise resolves so its container tree is released.
Routing Configuration
Each root needs its own router configuration:
RouterConfiguration (from @aurelia/router) registers RouteContext.setRoot under the hood, so avoid registering it twice in the same container tree. With independent Aurelia instances per root this is naturally enforced—just register the router in each register call that belongs to that root.
Shared Resources
Resources like custom elements, services, or value converters need to be registered in each application that uses them:
Alternative Approaches
While multi-root provides complete isolation, simpler alternatives may be sufficient for many use cases:
Router Hooks (For Authentication)
Dynamic Composition
Conditional Rendering
Choose multi-root when you need complete application isolation. For simpler scenarios, the alternatives above may be more appropriate.
Conclusion
Multi-root applications in Aurelia 2 provide complete isolation between different parts of your application, making them ideal for scenarios requiring separate application contexts, different configurations, or memory-intensive sections that benefit from being fully disposed. By following the steps above, you can create a robust multi-root setup that handles complex scenarios like public/private application transitions.
Last updated
Was this helpful?