Skip to content

State Management

Taras Demyanets edited this page Mar 31, 2026 · 1 revision

State Management

@microfrontend provides a mechanism to prevent accidental data loss when users navigate away from a microfrontend that has unsaved changes ("dirty state").

How It Works

1. Microfrontend reports dirty state    →  routedApp.changeState(true)
2. User tries to navigate away          →  router.go('other-app')
3. Shell checks for dirty state         →  Calls registered callback
4. Callback returns true/false          →  Navigation proceeds or is blocked
5. If navigation proceeds               →  Shell sends discard notification
6. Microfrontend resets its state       →  routedApp.changeState(false)

Reporting State from a Microfrontend

Use RoutedApp.changeState() to tell the shell whether the microfrontend has unsaved data:

// User starts editing a form — mark as dirty
routedApp.changeState(true, 'edit-form');

// User saves or cancels — mark as clean
routedApp.changeState(false, 'edit-form');
Parameter Type Description
hasState boolean true if there is unsaved state, false if clean
subRoute string | undefined The subroute associated with the state

Handling Navigation Confirmation in the Shell

Register a callback on the MetaRouter that decides whether to allow navigation away from a dirty microfrontend:

router.registerAllowStateDiscardCallbackAsync(
  async (metaRoute: string, subRoute?: string) => {
    // Show a confirmation dialog
    return confirm(`You have unsaved changes in ${metaRoute}. Discard?`);
  },
  MetaRouteStateEvaluation.RouteBased
);
  • If the callback returns true → navigation proceeds
  • If the callback returns false → navigation is rejected and the go() promise rejects

State Evaluation Modes

The MetaRouteStateEvaluation enum determines how the shell evaluates whether a microfrontend is dirty:

RouteBased (default)

The microfrontend is considered dirty only if the currently active subroute has reported state. If the user is on a different subroute within the same microfrontend, the state is not triggered.

router.registerAllowStateDiscardCallbackAsync(callback, MetaRouteStateEvaluation.RouteBased);

Use this when dirty state is tied to a specific view/page within the microfrontend.

AppBased

The microfrontend is considered dirty if any subroute has reported state. The specific subroute does not matter.

router.registerAllowStateDiscardCallbackAsync(callback, MetaRouteStateEvaluation.AppBased);

Use this when dirty state applies to the entire microfrontend, regardless of which subroute the user is on.

Handling State Discard in a Microfrontend

When the shell confirms navigation (the callback returned true), it sends a MessageStateDiscard to the microfrontend. Register a callback in the microfrontend to handle this:

routedApp.registerDiscardStateCallback(() => {
  // Reset form, clear local state, etc.
  form.reset();
  routedApp.changeState(false);
});

Complete Example

Shell

const router = new MetaRouter(config);

// Register state discard confirmation
router.registerAllowStateDiscardCallbackAsync(
  async (metaRoute, subRoute) => {
    // Custom confirmation dialog
    return new Promise((resolve) => {
      showDialog({
        title: 'Unsaved Changes',
        message: `Discard changes in ${metaRoute}${subRoute ? '/' + subRoute : ''}?`,
        onConfirm: () => resolve(true),
        onCancel: () => resolve(false)
      });
    });
  },
  MetaRouteStateEvaluation.RouteBased
);

Microfrontend

// Track form changes
form.addEventListener('input', () => {
  routedApp.changeState(true, 'edit');
});

// Handle save
saveButton.addEventListener('click', () => {
  saveData();
  routedApp.changeState(false, 'edit');
});

// Handle forced discard from the shell
routedApp.registerDiscardStateCallback(() => {
  form.reset();
  routedApp.changeState(false);
});

State Flow Diagram

Microfrontend                    Shell
     │                             │
     │──changeState(true, 'edit')─►│   User edits form
     │                             │
     │                             │   User clicks navigate
     │                             │──checkIfRoutingAllowed()
     │                             │   (has state? → yes)
     │                             │──callback('app-a', 'edit')
     │                             │   (user confirms → true)
     │                             │
     │◄─MessageStateDiscard────────│   Shell tells MF to discard
     │                             │
     │──changeState(false)────────►│   MF resets and reports clean
     │                             │
     │                             │──activateRoute('app-b')
     │                             │   Navigation proceeds

Clone this wiki locally