Skip to main content

Usage Guide

This guide provides detailed examples and patterns for using the PUI App SDK in your applications. For initial setup, see the Getting Started Guide.

Table of Contents

Routing

Set up routing with React Router and SDK components:

import { Routes, Route } from 'react-router-dom';
import { RequireAuth, useInjectSaga, Page } from '@elliemae/pui-app-sdk';
import { Layout } from './view/layout';
import { onSessionEvent } from './sideeffect/session';

const key = 'session';

export const AppRoutes = () => {
// Inject session management saga
useInjectSaga({ key, saga: onSessionEvent });

return (
<Routes>
<Route path="/" element={<Layout />}>
<Route
index
element={
<Page pageTitle="Dashboard">
<Dashboard />
</Page>
}
/>
<Route
path="details/:id"
element={
<Page pageTitle="Details">
<DetailsView />
</Page>
}
/>
</Route>
</Routes>
);
};

Lazy Loading Components

import { loadable } from '@elliemae/pui-app-sdk';

// Create loadable component
export const LoanView = loadable(() => import('./index'), {
fallback: <div>Loading...</div>,
});

// Use in routes
import { LoanView } from './view/loan-view/loadable';

const AppRoutes = () => (
<Routes>
<Route path="/loans" element={<LoanView />} />
</Routes>
);

Authentication

Handle authentication and logout with Redux Saga:

import {
auth,
globalConstants,
getRedirectUrl,
logout,
error,
} from '@elliemae/pui-app-sdk';
import { call, put, takeLatest } from 'redux-saga/effects';

function* endSessionHandler() {
try {
const cred = sessionStorage.getItem('Authorization') || '';
yield call(endSession, { token: cred });

// Trigger logout
yield put(
auth.logout({
clientId: globalConstants.CLIENT_ID,
redirectUri: getRedirectUrl(),
responseType: 'code',
scope: 'loc',
code: '1004',
}),
);
} catch (err) {
yield put(error.set({ messageText: (err as Error)?.message }));
}
}

export function* onSessionEvent() {
yield takeLatest(logout.CONFIRM, endSessionHandler);
}

API Integration with RTK Query

Recommended Approach: RTK Query is the preferred method for API integration, providing automatic caching, request deduplication, and simplified data fetching.

Setting Up RTK Query API

Create an API slice using RTK Query with the SDK's sdkBaseQuery:

import { createApi } from '@reduxjs/toolkit/query/react';
import { sdkBaseQuery } from '@elliemae/pui-app-sdk';

// Define API slice
export const resourcesApi = createApi({
reducerPath: 'resourcesApi',
baseQuery: sdkBaseQuery({ baseUrl: '/v1' }),
tagTypes: ['Resources'],
endpoints: (builder) => ({
getResources: builder.query<ResourceRecord[], void>({
query: () => ({
url: '/resources',
method: 'GET',
}),
providesTags: ['Resources'],
}),
getResourceById: builder.query<ResourceRecord, string>({
query: (id) => ({
url: `/resources/${id}`,
method: 'GET',
}),
providesTags: (result, error, id) => [{ type: 'Resources', id }],
}),
createResource: builder.mutation<ResourceRecord, CreateResourceRequest>({
query: (payload) => ({
url: '/resources',
method: 'POST',
data: payload,
}),
invalidatesTags: ['Resources'],
}),
updateResource: builder.mutation<
ResourceRecord,
{ id: string; data: UpdateResourceRequest }
>({
query: ({ id, data }) => ({
url: `/resources/${id}`,
method: 'PUT',
data,
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Resources', id }],
}),
deleteResource: builder.mutation<void, string>({
query: (id) => ({
url: `/resources/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Resources'],
}),
}),
});

export const {
useGetResourcesQuery,
useGetResourceByIdQuery,
useCreateResourceMutation,
useUpdateResourceMutation,
useDeleteResourceMutation,
} = resourcesApi;

Injecting RTK Query API

Use the SDK's useInjectQuery hook to dynamically inject the API:

import { useInjectQuery } from '@elliemae/pui-app-sdk';
import { resourcesApi } from './api/resources';

export const Layout = () => {
// Inject RTK Query API
useInjectQuery({ api: resourcesApi });

return (
<div>
<Header />
<Outlet />
</div>
);
};

Using RTK Query in Components

import {
useGetResourcesQuery,
useCreateResourceMutation,
useUpdateResourceMutation,
} from './api/resources';
import { waitMessageAction, useAppDispatch } from '@elliemae/pui-app-sdk';

function ResourceList() {
const dispatch = useAppDispatch();

// Fetch resources with automatic caching and refetching
const { data: resources, isLoading, error, refetch } = useGetResourcesQuery();

// Mutations
const [createResource, { isLoading: isCreating }] =
useCreateResourceMutation();
const [updateResource, { isLoading: isUpdating }] =
useUpdateResourceMutation();

// Show wait message while loading
useEffect(() => {
if (isLoading || isCreating || isUpdating) {
dispatch(waitMessageAction.open('Loading...', true));
} else {
dispatch(waitMessageAction.close());
}
}, [isLoading, isCreating, isUpdating, dispatch]);

const handleCreate = async () => {
try {
await createResource({
name: 'New Resource',
description: 'Description',
}).unwrap();
// Success - data automatically refetched due to cache invalidation
} catch (err) {
console.error('Failed to create resource:', err);
}
};

const handleUpdate = async (id: string) => {
try {
await updateResource({
id,
data: { name: 'Updated Name' },
}).unwrap();
} catch (err) {
console.error('Failed to update resource:', err);
}
};

if (error) return <div>Error loading resources</div>;
if (isLoading) return <div>Loading...</div>;

return (
<div>
<button onClick={handleCreate}>Create Resource</button>
{resources?.map((resource) => (
<div key={resource.id}>
<h3>{resource.name}</h3>
<button onClick={() => handleUpdate(resource.id)}>Update</button>
</div>
))}
</div>
);
}

Advanced RTK Query Features

Conditional Fetching

function ResourceDetails({ resourceId }: { resourceId?: string }) {
// Skip query if no resourceId
const { data, isLoading } = useGetResourceByIdQuery(resourceId!, {
skip: !resourceId,
});

return <div>{data?.name}</div>;
}

Polling

function ResourceList() {
// Poll every 30 seconds
const { data } = useGetResourcesQuery(undefined, {
pollingInterval: 30000,
});

return <div>{/* render resources */}</div>;
}

Optimistic Updates

const [updateResource] = useUpdateResourceMutation();

const handleUpdate = async (id: string, newName: string) => {
try {
await updateResource({
id,
data: { name: newName },
}).unwrap();
} catch (err) {
// Update failed, cache automatically reverted
}
};

Why RTK Query?

  • Automatic Caching: Reduces unnecessary network requests
  • Request Deduplication: Multiple components can use the same query without duplicate requests
  • Automatic Refetching: Data stays fresh with polling, refetch on focus, etc.
  • Optimistic Updates: Update UI before server response
  • Error Handling: Built-in error states
  • Loading States: Automatic loading indicators
  • TypeScript Support: Full type safety
  • Less Boilerplate: No need for actions, reducers, or sagas for API calls

HTTP Client (Legacy)

Note: For new projects, prefer RTK Query for API integration. Use the HTTP client directly only when you need fine-grained control or are working with existing code.

getAuthHTTPClient

Make authenticated API calls with automatic token management:

import { getAuthHTTPClient } from '@elliemae/pui-app-sdk';
import { logger } from './utils/logger';

const basePath = '/v1/resources';

export const getResources = async () => {
try {
const { data } = await getAuthHTTPClient().get(basePath);
return data;
} catch (err) {
logger.error({
message: 'Failed to get resources',
exception: err as Error,
});
throw err;
}
};

getHTTPClient

Create an HTTP client without automatic authorization headers. Useful for public endpoints or when you supply your own auth:

import { getHTTPClient } from '@elliemae/pui-app-sdk';

const publicClient = getHTTPClient({
baseURL: 'https://public-api.example.com',
headers: { 'X-Api-Key': 'my-key' },
sendLogRocketSessionHeader: true,
});

const { data } = await publicClient.get('/status');

Options:

OptionTypeDefaultDescription
baseURLstringApp config serviceEndpoints.apiBase URL for requests
headersobject{}Custom request headers
sendLogRocketSessionHeaderbooleanfalseAttach X-LogRocket-URL header

onAuthorizationFailure

Register a callback to handle 401 responses. The SDK will retry the failed request with the new authorization header you return:

import { onAuthorizationFailure } from '@elliemae/pui-app-sdk';

onAuthorizationFailure(async (error) => {
const newToken = await refreshAuthToken();
sessionStorage.setItem('Authorization', newToken);
return newToken;
});

If no handler is registered, a 401 response triggers endSession (redirect to login).

httpClientProps

Bind session management callbacks to the HTTP layer. AppRoot sets these automatically, but you can configure them manually when bootstrapping without AppRoot:

import {
httpClientProps,
resetUserIdleTime,
endSession,
} from '@elliemae/pui-app-sdk';

httpClientProps.resetUserIdleTime = resetUserIdleTime;
httpClientProps.endSession = endSession;

## State Management

> **Recommended**: For API-related state management, use [RTK Query](#api-integration-with-rtk-query) instead of manually creating actions, reducers, and sagas.

### Creating Actions and Reducers (Legacy Pattern)

For non-API state or when working with existing code, use Redux Toolkit with the SDK's API action creator:

```typescript
import { createSlice, PayloadActionCreator } from '@reduxjs/toolkit';
import { getApiActionCreator } from '@elliemae/pui-app-sdk';

const resourcesActionCreator = getApiActionCreator('resources');

type GetResourceActions = {
get: PayloadActionCreator<void, string>;
getSuccess: PayloadActionCreator<Array<ResourceRecord>, string>;
getError: PayloadActionCreator<void, string>;
};

type CreateResourceActions = {
create: PayloadActionCreator<void, string>;
createSuccess: PayloadActionCreator<CreateResourceResponse, string>;
createError: PayloadActionCreator<void, string>;
};

export const resources = {
...(resourcesActionCreator('create') as unknown as CreateResourceActions),
...(resourcesActionCreator('get') as unknown as GetResourceActions),
};

const resourcesSlice = createSlice({
name: 'resources',
initialState: [] as Array<ResourceRecord>,
reducers: {
getSuccess: (state, { payload }: { payload: Array<ResourceRecord> }) => {
state.push(...payload);
},
},
});

export const { reducer } = resourcesSlice;

Typed Selectors

Create typed selectors for better type safety:

import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { RootState } from '@elliemae/pui-app-sdk';
import { reducer } from './data/resources';

// Create typed selector
type AppState = RootState & { resources: ReturnType<typeof reducer> };
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;

Using State in Components

import { useAppDispatch, useAppSelector } from '@elliemae/pui-app-sdk';

function ResourceList() {
const resourcesData = useAppSelector((state) => state.resources);
const dispatch = useAppDispatch();

useEffect(() => {
if (!resourcesData.length) {
dispatch(resources.get());
}
}, [dispatch, resourcesData.length]);

return <div>{/* render resources */}</div>;
}

Layout Component with Dynamic Injection

Inject reducers and sagas at the layout level:

import { Outlet } from 'react-router-dom';
import { useInjectReducer, useInjectSaga } from '@elliemae/pui-app-sdk';
import { reducer } from './data/resources';
import { resourceSagas } from './sideeffect/resources';

export const Layout = () => {
// Inject reducer and saga at layout level
useInjectReducer({ key: 'resources', reducer });
useInjectSaga({ key: 'resources', saga: resourceSagas });

return (
<div>
<Header />
<Outlet />
</div>
);
};

Redux Saga Side Effects (Legacy Pattern)

Note: RTK Query eliminates the need for sagas in most API use cases. Use sagas for complex async workflows, WebSocket connections, or custom side effects not related to API calls.

Handle async operations with Redux Saga:

import { all, fork } from 'redux-saga/effects';
import { logger } from './utils/logger';
import { onGetAllResources } from './get';
import { onCreateResource } from './create';

export function* resourceSagas() {
try {
yield all([fork(onCreateResource), fork(onGetAllResources)]);
} catch (err) {
logger.error({
message: 'Saga initialization error',
exception: err as Error,
});
}
}

Redux Internals

These lower-level exports are pre-wired by the SDK. You only need them for advanced bootstrapping or when not using AppRoot.

getStore

Access the global Redux store outside of React components (e.g. in sagas, utilities, or test setup):

import { getStore } from '@elliemae/pui-app-sdk';

const store = getStore();
store.dispatch(someAction());
const state = store.getState();

authReducer & authSaga

The SDK includes a built-in auth reducer and saga that handle auth.login() / auth.logout() actions. They are automatically injected by AppRoot, but can be manually wired if needed:

import { authReducer, authSaga } from '@elliemae/pui-app-sdk';

// Manual injection (only if not using AppRoot)
useInjectReducer({ key: 'auth', reducer: authReducer });
useInjectSaga({ key: 'auth', saga: authSaga });

The auth saga listens for:

  • auth.login() — calls authorize() and dispatches LOGIN_SUCCESS
  • auth.logout() — calls endSession() with the provided params

errorMiddleware

Redux middleware that automatically dispatches error.set() for any RTK Query rejected action:

import { errorMiddleware } from '@elliemae/pui-app-sdk';

// Already included in configureStore() — no manual setup needed.
// Shown here for reference only.

When any RTK Query mutation or query is rejected with a value, the middleware extracts the error message and dispatches it to the global error toast.

createManager

Re-exported from redux-injectors. Create a standalone injector manager for manual reducer/saga injection outside React:

import { createManager } from '@elliemae/pui-app-sdk';

In most cases, use useInjectReducer / useInjectSaga hooks inside components instead.

Form Management

Build forms with validation using React Hook Form integration:

import { Form as SDKForm, useAppDispatch } from '@elliemae/pui-app-sdk';
import { useParams } from 'react-router-dom';
import { SubmitHandler } from 'react-hook-form';
import { resources } from './data/resources';

interface FormData {
firstName: string;
lastName: string;
email: string;
country?: { value: string; label: string };
}

export const MyForm = () => {
const dispatch = useAppDispatch();
const { id } = useParams<{ id: string }>();

const onSubmit: SubmitHandler<FormData> = (formData) => {
const { country, ...rest } = formData;

dispatch(
resources.save({
id,
...rest,
country: country?.value || country,
}),
);
};

return (
<SDKForm<FormData> onSubmit={onSubmit}>
<FormFields />
<SubmitButton />
</SDKForm>
);
};

Analytics

Track user events and page views using scripting objects:

import { CMicroApp } from '@elliemae/pui-app-sdk';
import { IAnalytics } from '@elliemae/pui-scripting-object';
import { logger } from './utils/logger';

let analyticsObj: IAnalytics | null = null;

export const getAnalytics = async () => {
if (!analyticsObj) {
try {
const app = CMicroApp.getInstance();
analyticsObj = (await app.getObject('analytics')) ?? null;
} catch (error) {
logger.error({
message: 'Error fetching analytics object',
exception: error as Error,
});
}
}
return analyticsObj;
};

export const getAppDetails = () => ({
appId: window.emui?.appId || '',
appUrl: window.location.href,
});

Timing Events

// Use in component
useEffect(() => {
(async () => {
const analytics = await getAnalytics();
if (analytics) {
await analytics.startTiming('PageLoad', getAppDetails());
// ... perform operation
await analytics.endTiming('PageLoad', getAppDetails());
}
})();
}, []);

Business Analytics Events

useEffect(() => {
if (recordId) {
(async () => {
const analytics = await getAnalytics();
if (analytics) {
await analytics.sendBAEvent({
event: 'RecordViewed',
recordId,
});
}
})();
}
}, [recordId]);

Direct Analytics Exports

The SDK also re-exports analytics utilities from @elliemae/pui-analytics-so:

import { Analytics, updateBAEventParameters } from '@elliemae/pui-app-sdk';
  • Analytics — the analytics singleton from pui-analytics-so. Prefer using the scripting object approach (above) instead of importing Analytics directly.
  • updateBAEventParameters — update the shared BA event parameters (e.g. instanceId, userId) that are merged into every sendBAEvent call. The SDK calls this automatically during authorize() and endSession().

sendBAEvent (deprecated)

Deprecated: Use Analytics.sendBAEvent() via the scripting object instead.

Legacy helper to push BA events to the GTM data layer:

import { sendBAEvent } from '@elliemae/pui-app-sdk';

sendBAEvent({
data: { eventName: 'button_click', eventCategory: 'user_interaction' },
self: true, // true = push to own dataLayer; false = send to host app
});

fetchUserSettings

Retrieve user settings from the API. Called automatically during authorize(), but available for manual use:

import { fetchUserSettings } from '@elliemae/pui-app-sdk';

const settings = await fetchUserSettings({
userName: 'jdoe',
isConsumerUser: false,
});

Micro-Frontends

Hosting Micro-Frontends

Use GuestMicroApp to embed child applications:

import { GuestMicroApp, history } from '@elliemae/pui-app-sdk';

// Simple guest micro-app component
const PricingMicroApp = () => <GuestMicroApp id="pricing" history={history} />;

export default PricingMicroApp;

Micro-Frontend Configuration

Define micro-apps in your app.config.json:

{
"microFrontendApps": {
"pricing": {
"name": "Pricing",
"hostUrl": "./pricing",
"development": {
"files": ["index.js"]
},
"production": {
"files": ["index.js"]
}
},
"services": {
"name": "Services",
"hostUrl": "./services",
"mode": "development",
"development": {
"files": ["index.dev.js", "index.css"]
},
"production": {
"files": ["index.js", "index.css"]
}
}
}
}

The GuestMicroApp component supports two history modes that control how the guest app's URL is managed. The mode is determined by the useParentHistory flag in the guest app's app.config.json.

useParentHistory: true (default) — Shared History

The guest app shares the host's browser history. Route changes in the guest are reflected in the host's URL bar and vice versa.

Host URL bar: https://host.example.com/auth-setup/loan/123
└─ basename ─┘└ guest route ┘

Host app (parent):

import { GuestMicroApp, history } from '@elliemae/pui-app-sdk';

const LoanApp = () => <GuestMicroApp id="loanapp" history={history} />;

Guest app config (app.config.json):

{
"useParentHistory": true
}

When to use: The guest app is the primary content area and its routes should be bookmarkable / shareable via the host's URL.


useParentHistory: false — Independent History

The guest app runs inside an iframe with its own browser history, independent of the host's URL. The host's URL bar does not change when the guest navigates internally.

Host URL bar: https://host.example.com/auth-setup (stays fixed)
Iframe URL: https://guest.example.com/dashboard (managed independently)

Guest app config (app.config.json):

{
"useParentHistory": false
}

When to use: The guest app should manage its own navigation without affecting the host's URL — e.g., an embedded tool, modal-like workflow, or self-contained feature.


Setting an Initial Route from the Host

The initialRoute prop lets the host app control which route the guest app lands on. This works with both history modes.

Scenario 1: Shared history with initial route

// Host navigates guest to /auth-setup/loan/123 on load
const LoanApp = () => (
<GuestMicroApp
id="loanapp"
history={history}
initialRoute="/auth-setup/loan/123"
/>
);

The guest app receives the parent's history, and the host's URL is set to the initial route.

Scenario 2: Independent history with initial route

// Host tells the guest to start at /app/dashboard
const MyApp = () => (
<GuestMicroApp id="myapp" history={history} initialRoute="/app/dashboard" />
);

The SDK defers the route push to the mount() phase (after the iframe is stable but before React renders), so the guest's React Router sees the correct URL on its first render — no flash of the default landing page.

Note: The initialRoute value should be the full path including the guest app's basename (e.g., /app/dashboard, not just /dashboard). The SDK pushes this path to the browser history directly, and React Router matches it against its configured basename.


Quick Reference

ScenariouseParentHistoryinitialRouteBehavior
Guest shares host URLtrue (default)omittedGuest routes reflected in host URL bar
Guest shares host URL, starts at specific pagetrue"/app/details/42"Host URL set to initial route on load
Guest has own URL spacefalseomittedGuest loads at its default route, host URL unchanged
Guest has own URL space, starts at specific pagefalse"/app/dashboard"Guest starts at /dashboard, host URL unchanged

Guest App app.config.json Example

{
"appId": "myapp",
"useParentHistory": false,
"activeEnv": "dev",
"microFrontendApps": {}
}

Micro-Frontend Host API

The SDK also provides host-side APIs for apps that manage guest micro-apps.

selfInitialize is Required

Both CMicroApp and CMicroAppHost now require selfInitialize: true in their constructor params. Calling getInstance() without this flag logs a deprecation warning at runtime:

CMicroApp.getInstance() called without selfInitialize: true — this legacy mode
is deprecated. Pass { selfInitialize: true } and use initialize() instead.

The legacy mode (where the constructor auto-chains loadAppConfig → getAppBridge → onInit) is still functional but will be removed in a future major release. Migrate by adding the flag and calling initialize() explicitly:

// Before (deprecated — triggers warning)
const app = CMicroApp.getInstance({ logger, onInit });

// After
const app = CMicroApp.getInstance({ logger, selfInitialize: true, onInit });
await app.initialize();

This applies to both hosts and guests. The explicit initialize() call gives you control over when config loading and host discovery happen, making the startup sequence predictable and easier to debug.

CMicroAppHost (Deprecated)

Deprecated — Use CMicroApp with selfInitialize: true instead. CMicroApp now supports both guest and host use cases. CMicroAppHost will be removed in a future major release.

CMicroAppHost is still available for backward compatibility but new apps should use CMicroApp:

// Deprecated
import { CMicroAppHost } from '@elliemae/pui-app-sdk';
const host = CMicroAppHost.getInstance({ logger, onInit });

// Use instead
import { CMicroApp } from '@elliemae/pui-app-sdk';
const app = CMicroApp.getInstance({ logger, selfInitialize: true, onInit });
await app.initialize();

enableReactAppForHostIntegration

Shorthand to create a CMicroApp (guest) instance for host-integration scenarios — useful when the host wants to render the guest inline:

import { enableReactAppForHostIntegration } from '@elliemae/pui-app-sdk';

const app = enableReactAppForHostIntegration({ onInit, onMount });

getMicroFrontEndAppConfig

Read a micro-app's resolved configuration from app.config.json. Returns the merged development/production config with absolute URLs:

import { getMicroFrontEndAppConfig } from '@elliemae/pui-app-sdk';

const config = getMicroFrontEndAppConfig({ id: 'pricing' });
// config.hostUrl, config.files, config.name, etc.

Build navigation links from all micro-apps defined in app.config.json:

import { getNavigationLinks } from '@elliemae/pui-app-sdk';

const links = getNavigationLinks();
// [{ id: 'pricing', name: 'Pricing', path: '/pricing' }, ...]

getLogger

Returns the logger from whichever micro-app context is active (host or guest):

import { getLogger } from '@elliemae/pui-app-sdk';

const logger = getLogger();
logger.info('Application ready');

Deprecated Components

MicroApp and MicroIFrameApp are deprecated. Use GuestMicroApp instead:

// Deprecated — will be removed in a future release
import { MicroApp, MicroIFrameApp } from '@elliemae/pui-app-sdk';

// Use instead
import { GuestMicroApp } from '@elliemae/pui-app-sdk';

Guest-Initiated Loading

By default, App Bridge drives the guest lifecycle: it calls init(), then mount(). With guest-initiated loading, the guest drives its own lifecycle by calling initialize() — the bridge only handles iframe creation and script injection.

Enabling Self-Initialize in app.config.json

Add "selfInitialize": true to the guest entry in the host's app.config.json. The GuestMicroApp component reads this flag automatically and passes it to App Bridge — no changes to JSX are needed.

{
"microFrontendApps": {
"loanapp": {
"name": "Loan",
"hostUrl": "https://encompass.d1.ice.com/loan",
"selfInitialize": true,
"production": {
"files": ["runtime~app.js", "vendors.js", "emui.js", "app.js"]
}
}
}
}

The <GuestMicroApp> component works exactly the same — no prop changes required:

import { GuestMicroApp } from '@elliemae/pui-app-sdk';

<GuestMicroApp
id="loanapp"
history={browserHistory}
homeRoute="/home"
onLoadComplete={(instanceId) => console.log('loaded', instanceId)}
/>;

Setting Up a Self-Initializing Guest

In the guest app, pass selfInitialize: true when creating the CMicroApp instance:

import { CMicroApp } from '@elliemae/pui-app-sdk';

const app = CMicroApp.getInstance({
logger,
onInit,
onMount,
onUnmount,
selfInitialize: true,
});

Then call initialize() in your app's entry point:

const params = await app.initialize();
// params contains moduleId, hostUrl, homeRoute, theme, history, etc.
// At this point onInit has been called and the app is ready to render.

initialize() discovers the host automatically and calls your onInit callback. If the host registered a module scripting object, initialize() fetches its parameters (home route, theme, history) and applies them. If no module SO is registered, initialize() logs a warning and proceeds — the guest can still render using its own app.config.json values.

tip

Use guest-initiated loading when the guest app needs to control its own startup sequence — for example, when it must load configuration or authenticate before rendering.

Standalone Mode

When the guest runs outside an iframe (e.g. during local development), initialize() detects standalone mode and skips host discovery. It loads app.config.json and calls onInit so the app can render normally.

Host Discovery (window.emui.getHost)

After the SDK discovers the host — whether via App Bridge (window.emui.__host) or SSF connect — it exposes a convenience accessor on window.emui:

const host = window.emui.getHost();

This works in both the self-initialize path (initialize()) and the host-initiated path (init()). The function returns the discovered host reference, or null if no host has been found yet.

SDK-based guests

For guests that use CMicroApp, prefer the instance method — it returns the same reference:

const app = CMicroApp.getInstance();
const host = app.getHost();

window.emui.getHost() is most useful for plain JS module guests (CDN scripts) that don't import the SDK:

const host = window.emui.getHost();
const moduleSO = await host.getObject('module');
const params = await moduleSO.getParameters();

Availability

ScenarioWhen getHost() is set
App Bridge iframe guestsImmediately when the iframe is created (set by the bridge), then replaced by the SDK after init() or initialize() completes
Self-initializing guestsAfter initialize() discovers the host
Host-initiated guestsAfter init() completes
Standalone mode (local dev)Not set — returns undefined

Module Scripting Object

The SDK provides helpers to create a module scripting object for guests that need host-provided parameters. This is optional — a self-initializing guest can work without it if it doesn't need parameters from the host.

Use a module SO when:

  • Shared single-page routing — the host passes its history object so route changes in the guest update the host's URL bar and vice versa (deep-linking, back/forward navigation across micro-apps)
  • Consistent theming — the host passes its styled-components theme so the guest renders with the same design tokens
  • Initial route — the host tells the guest which page to open on launch (e.g. /loans/123) without the guest flashing its default landing page first
  • Custom capabilities — the host can expose additional parameters (permissions, tenant context, feature flags) that the guest reads at startup

createGuestModule

Create a module scripting object for a guest app. It reads hostUrl, manifestPath, and homeRoute from app.config.json and merges them with host-provided values (theme, history, initialRoute).

Always register scripting objects through CMicroApp rather than directly on the app bridge — this ensures the object is added to both the App Bridge and the SSF Host:

import { CMicroApp, createGuestModule } from '@elliemae/pui-app-sdk';

// Host app — must use selfInitialize: true
const app = CMicroApp.getInstance({ selfInitialize: true, logger, onInit });
await app.initialize();

const moduleSO = createGuestModule('loanapp', {
theme: myTheme,
history: appHistory,
});

app.addScriptingObject(moduleSO, { guestId: 'loanapp' });

buildModuleParams

If you need to build the parameters object without creating a scripting object (e.g. for testing or custom SO implementations):

import { buildModuleParams } from '@elliemae/pui-app-sdk';

const params = buildModuleParams('loanapp', {
theme: myTheme,
history: appHistory,
initialRoute: '/loans/123',
});
// { moduleId: 'loanapp', hostUrl: '...', homeRoute: '/', theme: ..., history: ..., ... }

GuestModule Class

For advanced use cases where you need to subclass or override methods beyond what createGuestModule overrides support:

import { CMicroApp, GuestModule } from '@elliemae/pui-app-sdk';

class CustomModule extends GuestModule {
async getCapabilities() {
return { supportsCustomEvents: true };
}
}

const app = CMicroApp.getInstance({ selfInitialize: true, logger, onInit });
await app.initialize();

const mod = new CustomModule('loanapp', { theme: myTheme });
app.addScriptingObject(mod, { guestId: 'loanapp' });

Automatic Manifest Warm-Up

When getAppBridge() is called for the first time, the SDK automatically calls warmUp() for every micro-app listed in microFrontendApps. This pre-fetches their manifests in the background so that subsequent openApp() calls start faster.

No code changes are needed — this happens automatically when your app initializes the bridge.

Scripting Objects

Adding Scripting Objects

Access and share scripting objects between parent and child micro-apps:

import { CMicroApp } from '@elliemae/pui-app-sdk';
import { ScriptingObjectTypes } from '@elliemae/pui-scripting-object';

const addObject = async (
app: CMicroApp,
objectName: keyof ScriptingObjectTypes,
) => {
try {
const so = await app.getObject(objectName);
if (so) {
app.addScriptingObject(so);
}
} catch (error) {
// Handle error
}
};

// Get scripting objects from parent and expose to child
export const addScriptingObjects = async () => {
const app = CMicroApp.getInstance();
await Promise.all([
addObject(app, 'analytics'),
addObject(app, 'application'),
addObject(app, 'auth'),
addObject(app, 'http'),
addObject(app, 'loan'),
]);
};

// Call during app initialization
export const onInit: OnInitCallback = ({ history, homeRoute }) => {
// ... other initialization
addScriptingObjects().catch(() => {
// Handle error
});
};

Using Scripting Objects in Components

import { CMicroApp } from '@elliemae/pui-app-sdk';
import { useFormContext } from 'react-hook-form';

const listenToLoanSyncEvent = async (setValue) => {
const app = CMicroApp.getInstance();
const loan = await app.getObject('loan');

if (loan) {
const subscriptionId = app.subscribe({
eventId: 'loan.sync',
callback: ({ eventParams }) => {
const { firstname, lastname } = eventParams;
setValue('firstname', firstname);
setValue('lastname', lastname);

// Unsubscribe after handling
app.unsubscribe({
eventId: 'loan.sync',
token: subscriptionId,
});
},
});
}
};

export const FormComponent = () => {
const { setValue } = useFormContext();

useEffect(() => {
listenToLoanSyncEvent(setValue).catch(() => {
// Handle error
});
}, [setValue]);

return <div>{/* form fields */}</div>;
};

Wait Messages

Display loading indicators with customizable wait messages:

Using WaitMessage Component

import { waitMessageAction, useAppDispatch } from '@elliemae/pui-app-sdk';

// Show/hide wait message from anywhere in your app
function MyComponent() {
const dispatch = useAppDispatch();

const handleLoadData = async () => {
// Show wait message
dispatch(waitMessageAction.open('Loading data...', true));

try {
await fetchData();
} finally {
// Hide wait message
dispatch(waitMessageAction.close());
}
};

return <button onClick={handleLoadData}>Load Data</button>;
}

Wait Message Props

  • size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' - Size of the spinner
  • color: 'light' | 'dark' - Color theme
  • showText: boolean - Show loading text
  • withTooltip: boolean - Show as tooltip
  • tooltipStartPlacementPreference: Tooltip placement

Prompt users before navigating away from unsaved changes:

import {
NavigationPrompt,
NavigationPromptActions,
useAppDispatch,
} from '@elliemae/pui-app-sdk';

function FormPage() {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [showPrompt, setShowPrompt] = useState(false);
const dispatch = useAppDispatch();

// Listen for navigation prompt actions
useEffect(() => {
const handleConfirm = () => {
// Save data and proceed
saveData();
setShowPrompt(false);
};

const handleCancel = () => {
// Proceed without saving
setShowPrompt(false);
};

// Subscribe to actions
return () => {
// Cleanup
};
}, []);

// Show prompt when navigating with unsaved changes
const handleNavigation = () => {
if (hasUnsavedChanges) {
setShowPrompt(true);
}
};

return (
<>
<NavigationPrompt open={showPrompt} />
{/* Your form */}
</>
);
}

Dispatch Navigation Prompt Actions

import { NavigationPromptActions } from '@elliemae/pui-app-sdk';

// User confirms to save and continue
dispatch({ type: NavigationPromptActions.CONFIRM });

// User cancels or continues without saving
dispatch({ type: NavigationPromptActions.CANCEL });

Error Handling

Global Error Toast

Display error messages using the global error toast:

import { error, useAppDispatch } from '@elliemae/pui-app-sdk';

// Dispatch errors from anywhere in your app
function MyComponent() {
const dispatch = useAppDispatch();

const handleError = async () => {
try {
await riskyOperation();
} catch (err) {
dispatch(
error.set({
messageText: 'Operation failed',
description: (err as Error).message,
}),
);
}
};

return <button onClick={handleError}>Execute</button>;
}

Clear Errors

import { error } from '@elliemae/pui-app-sdk';

// Clear error state
dispatch(error.clear());

ARIA Live Messages

Announce messages to screen readers for accessibility:

import { ariaLive, useAppDispatch } from '@elliemae/pui-app-sdk';

function MyComponent() {
const dispatch = useAppDispatch();

const announceSuccess = () => {
dispatch(
ariaLive.announce({
message: 'Data saved successfully',
ariaLive: 'polite', // or 'assertive' for urgent messages
id: 'save-success',
}),
);
};

return <button onClick={announceSuccess}>Save</button>;
}

Decorators

The SDK provides TypeScript decorators for common patterns:

Function Decorators

import { decorators } from '@elliemae/pui-app-sdk';

const {
CacheUntilResolved,
Debounce,
Throttle,
Memoize,
MemoizeAsync,
RetryAsync,
AsyncSingleExecution,
QueueTask,
} = decorators.function;

class DataService {
// Cache async results until promise resolves
@CacheUntilResolved
async fetchData() {
return await api.getData();
}

// Debounce method calls
@Debounce(500)
onSearchInput(query: string) {
this.performSearch(query);
}

// Throttle method calls
@Throttle(1000)
onScroll() {
this.loadMoreData();
}

// Memoize synchronous results
@Memoize
calculateTotal(items: Item[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}

// Memoize async results
@MemoizeAsync
async fetchUserData(userId: string) {
return await api.getUser(userId);
}

// Retry failed async operations
@RetryAsync(3, 1000) // 3 retries, 1000ms delay
async unreliableOperation() {
return await api.flakeyEndpoint();
}

// Ensure only one execution at a time
@AsyncSingleExecution
async saveData() {
return await api.save();
}

// Queue tasks for sequential execution
@QueueTask
async processItem(item: Item) {
return await api.process(item);
}
}

Class Decorators

import { decorators } from '@elliemae/pui-app-sdk';

const { Singleton, Mixins } = decorators.class;

// Ensure only one instance exists
@Singleton
class ConfigService {
private config: Config;

constructor() {
this.config = loadConfig();
}
}

// Mix multiple classes
@Mixins(LoggingMixin, CachingMixin)
class DataManager {
// Inherits methods from both mixins
}

Advanced State Selectors

Using useStateSelector

Select specific fields from state with better performance:

import {
useStateSelector,
useStateSelectorShallow,
getSelectField,
} from '@elliemae/pui-app-sdk';

// In your slice file
export const selectUserState = getSelectField('user');

// In your component
function UserProfile() {
// Select single field
const userName = useStateSelector(selectUserState, 'profile.name', {
defaultValue: 'Guest',
});

// Select multiple fields
const [email, phone, address] = useStateSelector(
selectUserState,
['profile.email', 'profile.phone', 'profile.address'],
{ defaultValue: ['', '', ''] },
);

// Select nested objects
const settings = useStateSelector(selectUserState, 'preferences.settings', {
defaultValue: {},
});

return <div>{userName}</div>;
}

// Use shallow equality for better performance
function UserSettings() {
const settings = useStateSelectorShallow(selectUserState, 'preferences', {
defaultValue: {},
});

return <div>{/* render settings */}</div>;
}

Responsive Design

Media Query Hook

Respond to media query changes:

import { useMediaQueryList } from '@elliemae/pui-app-sdk';

function ResponsiveComponent() {
const deviceType = useMediaQueryList(
[
'(max-width: 767px)',
'(min-width: 768px) and (max-width: 1023px)',
'(min-width: 1024px)',
],
['mobile', 'tablet', 'desktop'],
'desktop', // default value
);

return (
<div>
{deviceType === 'mobile' && <MobileLayout />}
{deviceType === 'tablet' && <TabletLayout />}
{deviceType === 'desktop' && <DesktopLayout />}
</div>
);
}

Environment & URL Utilities

Environment Detection

import { isProd, isCIBuild } from '@elliemae/pui-app-sdk';

if (isProd()) {
// NODE_ENV === 'production'
}

if (isCIBuild()) {
// CI === 'true' (running in CI pipeline)
}

History Instances

The SDK exports two pre-created history instances:

import { history, memoryHistory } from '@elliemae/pui-app-sdk';

// history — browser history, used by default for routing
// memoryHistory — in-memory history, useful for testing or non-browser environments

Use memoryHistory in tests to avoid browser URL side-effects:

import { memoryHistory } from '@elliemae/pui-app-sdk';

memoryHistory.push('/test-route');

removeDoubleSlash

Collapse accidental double slashes in URLs (preserving the protocol ://):

import { removeDoubleSlash } from '@elliemae/pui-app-sdk';

removeDoubleSlash('https://example.com//api//v1');
// → 'https://example.com/api/v1'

Themes

Available theme constants:

import { Themes } from '@elliemae/pui-app-sdk';

// Themes.EM → 'em'
// Themes.CORP_COOL → 'corpcool'

Security Utilities

PII Redaction

Automatically redact personally identifiable information from logs:

import { redactPii } from '@elliemae/pui-app-sdk';

const sensitiveData = {
name: 'John Doe',
email: 'john@example.com',
phone: '555-123-4567',
ssn: '123-45-6789',
creditCard: '4532-1234-5678-9010',
address: '123 Main St, Apt 4B',
zipcode: '12345-6789',
};

const redacted = redactPii(sensitiveData);
console.log(redacted);
// {
// name: 'John Doe',
// email: '****',
// phone: '****',
// ssn: '****',
// creditCard: '****',
// address: '****',
// zipcode: '****',
// }

// Use when logging errors or data
logger.error(redactPii({ message: 'Error', data: sensitiveData }));

Listener Middleware

Use Redux Toolkit listener middleware for side effects:

import {
startSideEffect,
createSideEffect,
removeSideEffect,
clearSideEffects,
} from '@elliemae/pui-app-sdk';
import { resources } from './data/resources';

// Start listening for actions
startSideEffect({
actionCreator: resources.get,
effect: async (action, listenerApi) => {
// Access state
const state = listenerApi.getState();

// Call API
const data = await fetchResources();

// Dispatch success action
listenerApi.dispatch(resources.getSuccess(data));
},
});

// Create a reusable listener
const listener = createSideEffect({
actionCreator: resources.create,
effect: async (action, listenerApi) => {
await createResource(action.payload);
listenerApi.dispatch(resources.get());
},
});

// Remove a listener
removeSideEffect(listener);

// Clear all listeners
clearSideEffects();

Session Management

Session Timeout Handling

The SDK provides comprehensive session timeout management with warning and expiry callbacks:

import {
subscribeToSessionExpiryWarning,
subscribeToSessionExpiry,
subscribeToResetSession,
resetUserIdleTime,
trackActivity,
} from '@elliemae/pui-app-sdk';

// Subscribe to session expiry warning (shown before session expires)
subscribeToSessionExpiryWarning((warningNotifiedAt) => {
console.log('Session will expire soon!', warningNotifiedAt);
// Show warning modal to user
});

// Subscribe to session expiry event
subscribeToSessionExpiry(() => {
console.log('Session has expired');
// Redirect to login or handle session expiry
});

// Subscribe to session reset events
subscribeToResetSession((resetWarningModal) => {
if (resetWarningModal) {
// Close warning modal if it's open
}
});

// Manually reset user idle time (e.g., on user activity)
resetUserIdleTime(true); // Pass true to reset warning modal

// Track user activity (automatically resets idle timer)
trackActivity();

Session UI Components

SessionTimeout

A self-contained component that monitors idle time, displays a session-expiry warning modal, and dispatches logout.confirm() when the session expires. Place it once in your app root:

import { SessionTimeout } from '@elliemae/pui-app-sdk';

const Application = () => (
<AppRoot store={store} history={history} theme={theme} manageSession>
<SessionTimeout />
<AppRoutes />
</AppRoot>
);

AppRoot with manageSession renders SessionTimeout automatically. Add it manually only when assembling your own root layout.

WaitMessage

A full-screen loading indicator controlled by waitMessageAction Redux state:

import { WaitMessage } from '@elliemae/pui-app-sdk';

// Typically placed in AppRoot layout — shown/hidden via Redux
<WaitMessage size="xl" color="light" showText />;

The WaitMessage component reads its open/close state from Redux. Dispatch waitMessageAction.open() / waitMessageAction.close() to control it (see Wait Messages).

Initialize Session Management

import { listenStorageEvents, getAppConfigValue } from '@elliemae/pui-app-sdk';
import {
Environment,
Logger,
RuntimeLoggerOptions,
} from '@elliemae/pui-diagnostics';

// Initialize session management
listenStorageEvents();

// Configure logger with session data
export const onInit: OnInitCallback = ({ history, homeRoute }) => {
const sessionData = {
environment: getAppConfigValue<Environment>('activeEnv'),
appVersion: window.emui.version,
instanceId: sessionStorage.getItem('instanceId') || '',
userId: sessionStorage.getItem('userId') || '',
};

(logger as Logger).setOptions?.(sessionData as RuntimeLoggerOptions);
logger.info('Application initialized');

// ... rest of initialization
};

LogRocket Integration

Initialization

LogRocket is initialized by the host/loader layer (e.g. pui-app-loader, encw-loader) via @elliemae/pui-logrocket. Micro-apps do not need to call initLogRocket() themselves.

// Done by the loader — not by individual micro-apps
import { initLogRocket } from '@elliemae/pui-logrocket';

if (window.emui?.logRocketConfig?.appId) {
initLogRocket();
}

User Identification

The SDK automatically identifies users to LogRocket during the OAuth authorize() flow:

authorize() → getToken → introspectToken → sessionStorage.setItem('user', ...)
→ lrIdentify(userKey, { name, instanceId })
→ markLrIdentified(userKey) ← prevents re-identification

If LogRocket is not yet loaded when authorize() runs (common in encw-loader), the lrIdentify call is a no-op. The safety net is ensureLrIdentified inside @elliemae/pui-analytics-so — it automatically identifies the user from sessionStorage on the first lrTrackGuarded call after LogRocket becomes available.

Covered scenarios:

ScenarioHow identified
Fresh OAuth loginlrIdentify() in authorize()
Session restore (page refresh)ensureLrIdentified reads user from sessionStorage on first track
Late LogRocket loadensureLrIdentified fires when LR becomes available
Logout + new user loginresetLrIdentity() on logout clears state; new authorize() re-identifies
Duplicate tabFresh JS context; ensureLrIdentified reads from copied sessionStorage

Tracking Events

Use lrTrack from the SDK's logrocket utility for custom LogRocket events. It delegates to lrTrackGuarded from @elliemae/pui-analytics-so which provides:

  • Event buffering — events fired before LogRocket loads are queued (up to 20) and replayed once available
  • Sliding-window throttle — max 3 identical event+payload calls per second
  • Property-count warning — warns if more than 2 properties per event
import { lrTrack } from '../utils/logrocket';

// Custom event tracking
lrTrack('document-saved', { docId: '123' });

// Events before LR loads are queued and replayed later
lrTrack('login', { name: 'Jane', instanceId: 'inst-1' });

For business analytics events that should reach both GTM and LogRocket, use Analytics.sendBAEvent() via the scripting object — it calls lrTrackGuarded internally.

Logout

The endSession function handles LogRocket cleanup:

// Inside endSession() — already wired by the SDK
lrTrack('logout');
resetLrIdentity(); // clears identify state + pending event queue
sessionStorage.clear();

resetLrIdentity() ensures that:

  • The next session starts with a fresh identify state
  • Any queued events from the previous session are discarded

Service Worker

Initialize service worker for your app:

import { initServiceWorker } from '@elliemae/pui-app-sdk';
import { getBasePath } from './utils/paths';

// Initialize after creating CMicroApp instance
app = CMicroApp.getInstance({
logger,
onInit,
onMount,
onUnmount,
});

// Initialize service worker
initServiceWorker(getBasePath());

App Configuration

Loading Configuration

Load configuration from a JSON file:

import {
loadAppConfig,
setAppConfig,
getAppConfigValue,
} from '@elliemae/pui-app-sdk';

// Load from JSON file
await loadAppConfig('/path/to/app.config.json');

// Set configuration programmatically
setAppConfig({
appId: 'my-app',
activeEnv: 'production',
serviceEndpoints: {
api: 'https://api.example.com',
},
});

// Get specific config value
const apiEndpoint = getAppConfigValue('serviceEndpoints.api');
const appId = getAppConfigValue('appId');

Get Full Configuration

Retrieve the entire parsed config object. Useful when passing the config to other libraries (e.g. App Bridge):

import { getAppConfig } from '@elliemae/pui-app-sdk';

const config = getAppConfig();
// { appId: 'myapp', activeEnv: 'dev', microFrontendApps: { ... }, ... }

Update Configuration Values

import { setAppConfigValue } from '@elliemae/pui-app-sdk';

// Update individual config value
setAppConfigValue('activeEnv', 'staging');
setAppConfigValue('serviceEndpoints.api', 'https://staging-api.example.com');

Web Storage Events

Listen to storage events across tabs/windows:

import {
listenStorageEvents,
removeStorageEvents,
} from '@elliemae/pui-app-sdk';

// Start listening to storage events
listenStorageEvents();

// Stop listening
removeStorageEvents();

Micro-Frontend Communication

Host App Data Exchange

Exchange data between host and guest apps:

import {
setHostAppData,
getHostAppDataByKey,
fetchHostAppData,
sendMessageToHost,
} from '@elliemae/pui-app-sdk';

// In host app: Set data for guest apps
setHostAppData({ userId: '123', theme: 'dark' });

// In guest app: Get host data by key
const userId = getHostAppDataByKey('userId');

// Fetch all host data
const hostData = await fetchHostAppData();

// Send message to host app
sendMessageToHost({ type: 'UPDATE_THEME', payload: 'light' });

Guest Unload Handlers

Handle guest app unload events:

import {
onGuestUnloadStart,
notifyGuestUnloadComplete,
} from '@elliemae/pui-app-sdk';

// Register unload start handler
onGuestUnloadStart(() => {
console.log('Guest app is unloading');
// Cleanup resources
});

// Notify when unload is complete
notifyGuestUnloadComplete();

Render with Host Data

Render component with host app data:

import { renderWithHostData } from '@elliemae/pui-app-sdk';

renderWithHostData(<App />, {
hostData: { userId: '123' },
containerId: 'root',
});

Authentication Utilities

Auth Helper Functions

import {
isUserAuthorized,
login,
authorize,
endSession,
getAuthorizationHeader,
setLoginParams,
} from '@elliemae/pui-app-sdk';

// Check if user is authorized
if (isUserAuthorized()) {
// User is logged in
}

// Get authorization header for API calls
const authHeader = getAuthorizationHeader();
// Returns: 'Bearer <token>'

// Set login parameters
setLoginParams({
clientId: 'your-client-id',
redirectUri: 'https://your-app.com/callback',
scope: 'openid profile',
});

// Trigger login
await login();

// Trigger authorization
await authorize();

// End session (logout)
await endSession({
clientId: 'your-client-id',
redirectUri: 'https://your-app.com',
});

Auth Subpath Exports

The @elliemae/pui-app-sdk/auth subpath exports additional low-level functions not available on the root package:

import {
getIDPInfoFromUrl,
navigateToLoginPage,
} from '@elliemae/pui-app-sdk/auth';

getIDPInfoFromUrl

Extracts the authorization code and error_code from the current URL (set by the IDP after redirect):

const { code, idpErrorCode, redirectUri } = getIDPInfoFromUrl();
if (code) {
await authorize({ code, redirectUri, ... });
}

Redirects the browser to the IDP login page with the specified OAuth parameters:

navigateToLoginPage({
clientId: 'my-app',
redirectUri: window.location.href,
idpErrorCode: '',
scope: 'openid',
responseType: 'code',
instanceId: 12345, // optional
siteId: 1, // optional
userId: 'jdoe', // optional
disableRememberMe: true, // optional
useCompactIdpPage: false, // optional
isSSO: false, // optional
});

In most cases, use login() from the root package — it calls getIDPInfoFromUrl and navigateToLoginPage / authorize internally.

Require Auth Component

Protect routes requiring authentication:

import { RequireAuth } from '@elliemae/pui-app-sdk';

function App() {
return (
<RequireAuth>
<ProtectedContent />
</RequireAuth>
);
}

Utility Hooks

useInjectQuery

Inject RTK Query endpoints dynamically:

import { useInjectQuery } from '@elliemae/pui-app-sdk';
import { api } from './api';

function MyComponent() {
useInjectQuery({ key: 'users', endpoint: api.endpoints.getUsers });

// Use the endpoint
const { data } = api.endpoints.getUsers.useQuery();

return <div>{/* render data */}</div>;
}

useInjectSideEffect

Inject side effects dynamically:

import { useInjectSideEffect } from '@elliemae/pui-app-sdk';

function MyComponent() {
useInjectSideEffect({
key: 'analytics',
effect: (action, listenerApi) => {
// Track analytics
},
});

return <div>{/* component */}</div>;
}

useAppMiddleware

Access Redux middleware dynamically:

import { useAppMiddleware } from '@elliemae/pui-app-sdk';

function MyComponent() {
const middleware = useAppMiddleware();

// Use middleware

return <div>{/* component */}</div>;
}

Form Components

The SDK provides form field components with React Hook Form integration:

Available Form Components

import {
Form,
TextBox,
InputText,
LargeTextBox,
InputMask,
MaskedInputText,
ComboBox,
ComboBoxV3,
CheckBox,
Radio,
RadioGroup,
DateInput,
DatePicker,
DateTimePicker,
DateRangePicker,
Autocomplete,
Toggle,
FormSubmitButton,
FormItemLayout,
FormLayoutBlockItem,
ConnectForm,
FormPropsContext,
} from '@elliemae/pui-app-sdk';

TextBox

<TextBox
name="firstName"
label="First Name"
placeholder="Enter first name"
rules={{ required: 'First name is required' }}
/>

InputMask

import { InputMask, MASK_TYPES, MASK_PIPES } from '@elliemae/pui-app-sdk';

<InputMask
name="phone"
label="Phone Number"
mask={MASK_TYPES.PHONE}
pipe={MASK_PIPES.PHONE}
/>;

ComboBox

<ComboBox
name="country"
label="Country"
options={[
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
]}
rules={{ required: 'Please select a country' }}
/>

DatePicker

<DatePicker
name="birthDate"
label="Date of Birth"
rules={{ required: 'Date of birth is required' }}
/>

DateRangePicker

<DateRangePicker
name="dateRange"
label="Select Date Range"
startLabel="Start Date"
endLabel="End Date"
/>

CheckBox

<CheckBox
name="agreeToTerms"
label="I agree to the terms and conditions"
rules={{ required: 'You must agree to continue' }}
/>

RadioGroup

<RadioGroup name="paymentMethod" label="Payment Method">
<Radio value="credit" label="Credit Card" />
<Radio value="debit" label="Debit Card" />
<Radio value="paypal" label="PayPal" />
</RadioGroup>

Toggle

<Toggle name="notifications" label="Enable Notifications" />

Autocomplete

<Autocomplete
name="city"
label="City"
options={cities}
onInputChange={(value) => fetchCities(value)}
/>

Form Layout

<Form onSubmit={handleSubmit}>
<FormItemLayout>
<TextBox name="firstName" label="First Name" />
<TextBox name="lastName" label="Last Name" />
</FormItemLayout>

<FormLayoutBlockItem>
<LargeTextBox name="comments" label="Comments" rows={4} />
</FormLayoutBlockItem>

<FormSubmitButton>Submit</FormSubmitButton>
</Form>

Reading the React Hook Form Configuration (FormPropsContext)

<Form> builds its useForm instance from the reactHookFormProps prop ({ mode: 'onBlur' } by default) and exposes that configuration to descendants through FormPropsContext. Use it when a child needs to read the original UseFormProps (e.g. mode) without prop-drilling.

import { useContext } from 'react';
import { FormPropsContext } from '@elliemae/pui-app-sdk';

const MyCustomSubmitButton = () => {
const mode = useContext(FormPropsContext)?.mode;

return <button disabled={mode !== 'onSubmit'}>Submit</button>;
};

Note: Earlier versions of the SDK encouraged casting useFormContext() to a CustomFormProviderProps shape to read formProps.mode. That pattern stopped working with react-hook-form@7.x, whose FormProvider only forwards a fixed allow-list of UseFormReturn fields into context. Migrate any such consumer to useContext(FormPropsContext).

Storybook Integration

Decorate Storybook stories with app context:

import { withAppDecorator } from '@elliemae/pui-app-sdk';

export default {
title: 'Components/MyComponent',
component: MyComponent,
decorators: [withAppDecorator],
};

Utility Components

VisuallyHidden

Hide content visually but keep it accessible to screen readers:

import { VisuallyHidden } from '@elliemae/pui-app-sdk';

<VisuallyHidden>
<label htmlFor="search">Search</label>
</VisuallyHidden>;

Page Component

Set page title and metadata:

import { Page } from '@elliemae/pui-app-sdk';

<Page pageTitle="Dashboard">
<DashboardContent />
</Page>;

Testing

Testing with Redux

The SDK provides testing helpers for components with Redux:

import {
renderWithRouterRedux,
renderWithRedux,
renderWithRouter,
} from '@elliemae/pui-app-sdk';
import { screen } from '@testing-library/react';

describe('MyComponent', () => {
it('renders with Redux state', () => {
renderWithRedux(<MyComponent />, {
initialState: { user: { name: 'John' } },
});

expect(screen.getByText('John')).toBeInTheDocument();
});

it('renders with Router and Redux', () => {
renderWithRouterRedux(<MyComponent />, {
initialState: { resources: [] },
route: '/resources',
});

expect(screen.getByTestId('resource-list')).toBeInTheDocument();
});
});

Testing Library Re-exports

The SDK re-exports common @testing-library/react utilities so you can import everything from one place:

import {
render, // custom render wrapped with AppRoot + theme + store
screen,
waitFor,
waitForElementToBeRemoved,
within,
fireEvent,
act,
cleanup,
renderHook,
} from '@elliemae/pui-app-sdk';

The custom render wraps components in AppRoot with a default store and theme. For more control, use renderWithRedux or renderWithRouterRedux.

RenderWithStateAddOns

A component that injects reducers, sagas, and theme in one shot — useful in Storybook stories or integration tests:

import { RenderWithStateAddOns } from '@elliemae/pui-app-sdk';

<RenderWithStateAddOns
Component={MyComponent}
reducer={{ key: 'resources', reducer: resourcesReducer }}
saga={{ key: 'resources', saga: resourcesSaga }}
theme={myTheme}
someProp="value"
/>;

Accepts single or array values for reducer and saga:

<RenderWithStateAddOns
Component={Dashboard}
reducer={[
{ key: 'resources', reducer: resourcesReducer },
{ key: 'users', reducer: usersReducer },
]}
saga={[
{ key: 'resources', saga: resourcesSaga },
{ key: 'users', saga: usersSaga },
]}
/>

Common Patterns

Error Boundaries

import { ErrorBoundary, ErrorToast } from '@elliemae/pui-app-sdk';

function App() {
return (
<ErrorBoundary>
<ErrorToast />
<YourApp />
</ErrorBoundary>
);
}

API Error Handling with RTK Query

RTK Query provides automatic error handling with built-in error states:

import { useGetResourceQuery } from './api/resources';
import { error } from '@elliemae/pui-app-sdk';
import { useAppDispatch } from '@elliemae/pui-app-sdk';

function ResourceDetails({ id }: { id: string }) {
const dispatch = useAppDispatch();
const { data, error: queryError, isError } = useGetResourceQuery(id);

useEffect(() => {
if (isError && queryError) {
dispatch(
error.set({
messageText: 'Failed to load resource. Please try again.',
}),
);
}
}, [isError, queryError, dispatch]);

if (isError) return <div>Error loading resource</div>;

return <div>{data?.name}</div>;
}

API Error Handling (Legacy Pattern)

For direct HTTP client usage, always wrap API calls with proper error handling:

export const getResource = async (id: string) => {
try {
const { data } = await getAuthHTTPClient().get(`/api/resource/${id}`);
return data;
} catch (err) {
logger.error({
message: 'Failed to get resource',
exception: err as Error,
});
throw new Error(
`Unable to get resource. Please try again later. Details: ${
(err as Error)?.message
}`,
);
}
};

Type Exports

The SDK exports these TypeScript types for use in your application:

import type {
RootState, // Root Redux state type
AppStore, // Store type returned by configureStore
AppDispatch, // Dispatch type for the store
AppConfig, // Shape of app.config.json
EMUI, // Shape of window.emui
Await, // Utility: unwrap Promise<T> to T
GuestProps, // Props available on CMicroApp instance
OnInitCallback, // Signature for CMicroApp onInit
OnMountCallback, // Signature for CMicroApp onMount
OnUnMountCallback, // Signature for CMicroApp onUnmount
OnHostInitCallback, // Signature for CMicroAppHost onInit
HostProvidedParams, // Host-provided values for module SO
ModuleOverrides, // Method overrides for GuestModule
UseStateSelectorOptions, // Options for useStateSelector
SelectStateFieldFunction, // Return type of getSelectField
} from '@elliemae/pui-app-sdk';

Extend RootState for typed selectors when you inject additional reducers:

import type { RootState } from '@elliemae/pui-app-sdk';
import { reducer } from './data/resources';

type AppState = RootState & { resources: ReturnType<typeof reducer> };

Troubleshooting

Store Not Initialized

Ensure your store is initialized in the onInit callback:

export const onInit: OnInitCallback = ({ history, homeRoute }) => {
store = configureStore({} as RootState, history);
};

Saga Not Running

Make sure to inject sagas in your layout or root component:

import { useInjectSaga } from '@elliemae/pui-app-sdk';

export const Layout = () => {
useInjectSaga({ key: 'resources', saga: resourceSagas });
// ...
};

Scripting Objects Not Available

Ensure scripting objects are added during initialization:

export const onInit: OnInitCallback = ({ history, homeRoute }) => {
// ... store initialization
addScriptingObjects().catch(() => {
logger.error('Failed to add scripting objects');
});
};

Form Validation Issues

For field-level validation, ensure you're passing correct rules:

<TextBox
name="email"
rules={{
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
}}
/>

Host-Guest Communication

For communication between host and guest apps:

// In guest app
import { sendMessageToHost } from '@elliemae/pui-app-sdk';

sendMessageToHost({ type: 'UPDATE', payload: data });

// In host app
import { fetchHostAppData } from '@elliemae/pui-app-sdk';

const hostData = await fetchHostAppData();