Skip to content

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 });
/>
  1. <AppView> fetches the bundle JS and optional CSS from the platform's bundle CDN.
  2. It evaluates the bundle, which registers the view via registerAppView().
  3. The component mounts the remote view into an isolated Shadow DOM element, preventing CSS bleed in both directions.
  4. On unmount, AppView calls the contract's unmount() and removes the injected script tag.

Exposing a View from a Forge App

Install @ptkl/components in the bundle app:

npm install @ptkl/components

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

registerAppView(name: string, render: () => ReactNode, options?: AppViewOptions): void
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:

  1. Sets an error state and renders the default error UI (a styled role="alert" box).
  2. Calls onError(error) if provided.
  3. Renders errorFallback(error) instead of the default error UI if provided.

Common causes of a missing mount():

  • The bundle does not call registerAppView().
  • The name passed to registerAppView() does not match the view prop on <AppView>.
  • The bundle threw an exception before reaching registerAppView().