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
- Authentication
- API Integration with RTK Query
- HTTP Client
- State Management
- Form Management
- Analytics
- Micro-Frontends
  - Navigation & History Modes
- Scripting Objects
- Wait Messages
- Navigation Prompts
- Error Handling
- ARIA Live Messages
- Decorators
- Advanced State Selectors
- Responsive Design
- Security Utilities
- Listener Middleware
- Session Management
- App Configuration
- Web Storage Events
- Micro-Frontend Communication
- Authentication Utilities
- Utility Hooks
- Form Components
- Storybook Integration
- Utility Components
- Testing
- Common Patterns
- Troubleshooting

## 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 { waitMessage } from '@elliemae/pui-app-sdk';
    import { 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(waitMessage.open({ message: 'Loading...' }));
        } else {
          dispatch(waitMessage.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.

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 interface GetResourcesResponse {
      id: string;
      name: string;
      // ... other fields
    }

    export const getResources = async () => {
      try {
        const { data }: { data: Array<GetResourcesResponse> } =
          await getAuthHTTPClient().get(basePath);
        return data;
      } catch (err) {
        logger.error({
          message: 'Failed to get resources',
          exception: err as Error,
        });
        throw new Error(
          `Unable to get resources. Please try again later. Details: ${
            (err as Error)?.message
          }`,
        );
      }
    };

    export const createResource = async (payload: CreateResourceRequest) => {
      try {
        const { data }: { data: CreateResourceResponse } =
          await getAuthHTTPClient().post(`${basePath}`, payload);
        return data;
      } catch (err) {
        logger.error({
          message: 'Failed to create resource',
          exception: err as Error,
        });
        throw new Error(
          `Unable to create resource. Please try again later. Details: ${
            (err as Error)?.message
          }`,
        );
      }
    };

## State Management​

> Recommended: For API-related state management, use 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:

    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,
        });
      }
    }

## 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]);

## 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"]
          }
        }
      }
    }

### Navigation & History Modes​

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​

Scenario | `useParentHistory` | `initialRoute` | Behavior
--- | --- | --- | ---
Guest shares host URL | `true` (default) | omitted | Guest routes reflected in host URL bar
Guest shares host URL, starts at specific page | `true` | `"/app/details/42"` | Host URL set to initial route on load
Guest has own URL space | `false` | omitted | Guest loads at its default route, host URL unchanged
Guest has own URL space, starts at specific page | `false` | `"/app/dashboard"` | Guest starts at `/dashboard`, host URL unchanged

#### Guest App `app.config.json` Example​

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

## 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

## Navigation Prompts​

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 { navigationPrompt } from '@elliemae/pui-app-sdk';

    // User confirms to save and continue
    dispatch(navigationPrompt.confirm());

    // User cancels or continues without saving
    dispatch(navigationPrompt.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>
      );
    }

## 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 Renewal​

Automatically renew sessions with token introspection:

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

    // Manually renew session
    await renewSession();

### 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​

Initialize LogRocket for session recording:

    import { initLogRocket } from '@elliemae/pui-logrocket';

    // Initialize LogRocket if configured
    if (window.emui?.logRocketConfig?.appId) {
      initLogRocket();
    }

### 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');

### 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',
    });

### 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,
    } 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>

## 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();
      });
    });

## 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
          }`,
        );
      }
    };

## 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();