AppView — Embedding Views Across Apps
Every Forge app's main view is its primary entry point, but apps can expose additional named views that other apps embed using the <AppView> component. This lets a single bundle own a piece of UI that multiple apps can render inside their own screens.
How It Works
Consumer app (React) Remote bundle (Forge app)
────────────────────── ──────────────────────────
<AppView registerAppView("onboarding",
scope="crm" ──→ () => <App />,
view="onboarding" { styles });
/>
<AppView>fetches the bundle JS and optional CSS from the platform's bundle CDN.- It evaluates the bundle, which registers the view via
registerAppView(). - The component mounts the remote view into an isolated Shadow DOM element, preventing CSS bleed in both directions.
- On unmount,
AppViewcalls the contract'sunmount()and removes the injected script tag.
Exposing a View from a Forge App
Install @ptkl/components in the bundle app:
Then call registerAppView at the top level of the bundle's entry file — never inside a component:
// main.tsx (built as a standalone bundle via Vite library mode)
import { registerAppView } from '@ptkl/components';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
registerAppView('onboarding', () => (
<BrowserRouter>
<App />
</BrowserRouter>
));
Declaring the View in the Manifest
Add the view's entry file under views in ptkl.config.js:
export default {
name: 'crm',
version: '1.0.0',
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
onboarding: path.resolve(__dirname, 'src/onboarding/main.tsx'),
},
// ...
}
Each key in views becomes an independently-built bundle:
{scope}/{view}.bundle.js and {scope}/{view}.css.
Consuming a View in Another App
Import AppView from @ptkl/components and point it at the remote scope and view name:
import { AppView } from '@ptkl/components';
export function OnboardingPage() {
return (
<AppView
scope="crm"
view="onboarding"
fallback={<p>Loading…</p>}
onError={(err) => console.error(err)}
/>
);
}
<AppView> Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
scope |
string |
✅ | — | The Forge app name (e.g. "crm"). |
view |
string |
✅ | — | The view name as declared in the manifest and passed to registerAppView(). |
env |
string |
sessionStorage.getItem("forge_app_env") ?? "dev" |
Deployment environment ("dev" or "prod"). |
|
fallback |
ReactNode |
null |
Rendered while the bundle is loading. | |
onError |
(error: Error) => void |
— | Called when the bundle fails to load or the view contract is missing. | |
errorFallback |
(error: Error) => ReactNode |
Built-in error UI | Custom error state renderer. | |
className |
string |
— | Applied to the container <div>. |
|
style |
CSSProperties |
— | Inline styles on the container <div>. |
registerAppView API
| Parameter | Type | Description |
|---|---|---|
name |
string |
Must match the view prop on <AppView> and the key in the manifest. |
render |
() => ReactNode |
Factory function that returns the root React element. Called once at mount time. |
Internally, registerAppView sets window[name] to an AppViewContract object with mount() and unmount() methods. AppView reads that contract to control the remote view's lifecycle.
Style Isolation
Each view runs in a fully isolated environment — styles from the host app do not affect the remote view, and the remote view's styles do not affect the host app. No extra configuration is needed; it works automatically.
Bundle Caching
Bundles and CSS files are cached in memory for the lifetime of the host app's session. If <AppView> remounts with the same scope / view / env combination, no second network request is made.
Error Handling
If the bundle fails to load (network error, 404, etc.) or window[view] does not expose a valid mount() function after evaluation, AppView:
- Sets an error state and renders the default error UI (a styled
role="alert"box). - Calls
onError(error)if provided. - Renders
errorFallback(error)instead of the default error UI if provided.
Common causes of a missing mount():
- The bundle does not call
registerAppView(). - The
namepassed toregisterAppView()does not match theviewprop on<AppView>. - The bundle threw an exception before reaching
registerAppView().