Files
ISA-Frontend/docs/guidelines/state-management.md
Lorenz Hilpert 596ae1da1b Merged PR 1969: Reward Shopping Cart Implementation with Navigation State Management and Shipping Address Integration
1. Reward Shopping Cart Implementation
  - New shopping cart with quantity control and availability checking
  - Responsive shopping cart item component with improved CSS styling
  - Shipping address integration in cart
  - Customer reward card and billing/shipping address components

  2. Navigation State Management Library (@isa/core/navigation)
  - New library with type-safe navigation context service (373 lines)
  - Navigation state service (287 lines) for temporary state between routes
  - Comprehensive test coverage (668 + 227 lines of tests)
  - Documentation (792 lines in README.md)
  - Replaces query parameters for passing temporary navigation context

  3. CRM Shipping Address Services
  - New ShippingAddressService with fetching and validation
  - CustomerShippingAddressResource and CustomerShippingAddressesResource
  - Zod schemas for data validation

  4. Additional Improvements
  - Enhanced searchbox accessibility with ARIA support
  - Availability data access rework for better fetching/mapping
  - Storybook tooltip variant support
  - Vitest JUnit and Cobertura reporting configuration

Related work items: #5382, #5383, #5384
2025-10-15 14:59:34 +00:00

12 KiB

State Management

Local State

  • Use Signals for component-level state
  • Keep state close to where it's used
  • Document state management decisions

Global State (NgRx)

  • Use for complex application state
  • Follow feature-based store organization
  • Implement proper error handling

Navigation State

Navigation state refers to temporary data preserved between routes during navigation flows. Unlike global state or local component state, navigation state is transient and tied to a specific navigation flow within a tab.

Storage Architecture

Navigation contexts are stored in tab metadata using @isa/core/navigation:

// Context stored in tab metadata:
tab.metadata['navigation-contexts'] = {
  'default': {
    data: { returnUrl: '/cart', customerId: 123 },
    createdAt: 1234567890
  },
  'customer-flow': {
    data: { step: 2, selectedOptions: ['A', 'B'] },
    createdAt: 1234567895
  }
}

Benefits:

  • Automatic cleanup when tabs close (no manual cleanup needed)
  • Tab isolation (contexts don't leak between tabs)
  • Persistence across page refresh (via TabService UserStorage)
  • Integration with tab lifecycle management

When to Use Navigation State

Use @isa/core/navigation for:

  • Return URLs: Storing the previous route to navigate back to
  • Wizard/Multi-step Forms: Passing context between wizard steps
  • Context Preservation: Maintaining search queries, filters, or selections when drilling into details
  • Temporary Data: Any data needed only for the current navigation flow within a tab

When NOT to Use Navigation State

Avoid using navigation state for:

  • Persistent Data: Use NgRx stores or services instead
  • Shareable URLs: Use route parameters or query parameters if the URL should be bookmarkable
  • Long-lived State: Use session storage or NgRx with persistence
  • Cross-tab Communication: Use services with proper state management

Best Practices

Do Use Navigation Context (Tab Metadata)

// Good: Clean URLs, automatic cleanup, tab-scoped
navState.preserveContext({
  returnUrl: '/customer-list',
  searchQuery: 'John Doe',
  context: 'reward-selection'
});

await router.navigate(['/customer', customerId]);

// Later (after intermediate navigations):
const context = navState.restoreAndClearContext<{ returnUrl: string }>();
await router.navigateByUrl(context.returnUrl);

Don't Use Query Parameters for Temporary State

// Bad: URL pollution, can be overwritten, visible in browser bar
await router.navigate(['/customer', customerId], {
  queryParams: {
    returnUrl: '/customer-list',
    searchQuery: 'John Doe'
  }
});
// URL becomes: /customer/123?returnUrl=%2Fcustomer-list&searchQuery=John%20Doe

Don't Use Router State for Multi-Step Flows

// Bad: Lost after intermediate navigations
await router.navigate(['/customer/search'], {
  state: { returnUrl: '/reward/cart' }  // Lost after next navigation!
});

Integration with TabService

Navigation context relies on TabService for automatic tab scoping:

// Context automatically scoped to active tab
const tabId = tabService.activatedTabId();  // e.g., 123

// When you preserve context:
navState.preserveContext({ returnUrl: '/cart' });
// Stored in: tab[123].metadata['navigation-contexts']['default']

// When you preserve with custom scope:
navState.preserveContext({ step: 2 }, 'wizard-flow');
// Stored in: tab[123].metadata['navigation-contexts']['wizard-flow']

Automatic Cleanup: When the tab closes, all contexts stored in that tab's metadata are automatically removed. No manual cleanup required!

Usage Example: Multi-Step Flow

Start of Flow (preserving context):

import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { NavigationStateService } from '@isa/core/navigation';

export class CustomerListComponent {
  private router = inject(Router);
  private navState = inject(NavigationStateService);

  async viewCustomerDetails(customerId: number) {
    // Preserve context before navigating
    this.navState.preserveContext({
      returnUrl: this.router.url,
      searchQuery: this.searchForm.value.query
    });

    await this.router.navigate(['/customer', customerId]);
  }
}

Intermediate Navigation (context persists):

export class CustomerDetailsComponent {
  private router = inject(Router);

  async editAddress(addressId: number) {
    // Context still preserved through intermediate navigations
    await this.router.navigate(['/address/edit', addressId]);
  }
}

End of Flow (restoring context):

import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { NavigationStateService } from '@isa/core/navigation';

interface CustomerNavigationContext {
  returnUrl: string;
  searchQuery?: string;
}

export class AddressEditComponent {
  private router = inject(Router);
  private navState = inject(NavigationStateService);

  async complete() {
    // Restore and clear context
    const context = this.navState.restoreAndClearContext<CustomerNavigationContext>();

    if (context?.returnUrl) {
      await this.router.navigateByUrl(context.returnUrl);
    } else {
      // Fallback navigation
      await this.router.navigate(['/customers']);
    }
  }

  // For template usage
  hasReturnUrl(): boolean {
    return this.navState.hasPreservedContext();
  }
}

Template:

@if (hasReturnUrl()) {
  <button (click)="complete()">Zurück</button>
}

Advanced: Multiple Concurrent Flows

Use custom scopes to manage multiple flows in the same tab:

export class DashboardComponent {
  private navState = inject(NavigationStateService);
  private router = inject(Router);

  async startCustomerFlow() {
    // Save context for customer flow
    this.navState.preserveContext(
      { returnUrl: '/dashboard', flowType: 'customer' },
      'customer-flow'
    );

    await this.router.navigate(['/customer/search']);
  }

  async startProductFlow() {
    // Save context for product flow (different scope)
    this.navState.preserveContext(
      { returnUrl: '/dashboard', flowType: 'product' },
      'product-flow'
    );

    await this.router.navigate(['/product/search']);
  }

  async completeCustomerFlow() {
    // Restore customer flow context
    const context = this.navState.restoreAndClearContext('customer-flow');
    if (context?.returnUrl) {
      await this.router.navigateByUrl(context.returnUrl);
    }
  }

  async completeProductFlow() {
    // Restore product flow context
    const context = this.navState.restoreAndClearContext('product-flow');
    if (context?.returnUrl) {
      await this.router.navigateByUrl(context.returnUrl);
    }
  }
}

Architecture Decision

Why Tab Metadata instead of SessionStorage?

Tab metadata provides several advantages over SessionStorage:

  • Automatic Cleanup: Contexts are automatically removed when tabs close (no manual cleanup or TTL management needed)
  • Better Integration: Seamlessly integrated with tab lifecycle and TabService
  • Tab Isolation: Impossible to leak contexts between tabs (scoped by tab ID)
  • Simpler Mental Model: Contexts are "owned" by tabs, not stored globally
  • Persistence: Tab metadata persists across page refresh via UserStorage

Why not Query Parameters?

Query parameters were traditionally used for passing state, but they have significant drawbacks:

  • URL Pollution: Makes URLs long, ugly, and non-bookmarkable
  • Overwritable: Intermediate navigations can overwrite query parameters
  • Security: Sensitive data visible in browser URL bar
  • User Experience: Affects URL sharing and bookmarking

Why not Router State?

Angular's Router state mechanism has limitations:

  • Lost After Navigation: State is lost after the immediate navigation (doesn't survive intermediate navigations)
  • Not Persistent: Doesn't survive page refresh
  • No Tab Scoping: Can't isolate state by tab

Why Tab Metadata is Better:

Navigation context using tab metadata provides:

  • Survives Intermediate Navigations: State persists across multiple navigation steps
  • Clean URLs: No visible state in the URL bar
  • Reliable: State survives page refresh (via TabService UserStorage)
  • Type-safe: Full TypeScript support with generics
  • Platform-agnostic: Works with SSR/Angular Universal
  • Automatic Cleanup: No manual cleanup needed when tabs close
  • Tab Isolation: Contexts automatically scoped to tabs

Comparison with Other State Solutions

Feature Navigation Context NgRx Store Service State Query Params Router State
Scope Tab-scoped flow Application-wide Feature-specific URL-based Single navigation
Persistence Until tab closes Configurable Component lifetime URL lifetime Lost after nav
Survives Refresh Yes ⚠️ Optional No Yes No
Survives Intermediate Navs Yes Yes Yes ⚠️ Sometimes No
Automatic Cleanup Yes (tab close) Manual Manual N/A Yes
Tab Isolation Yes No No No No
Clean URLs Yes N/A N/A No Yes
Shareability No No No Yes No
Type Safety Yes Yes Yes ⚠️ Limited Yes
Use Case Multi-step flow Global state Feature state Bookmarkable state Simple navigation

Best Practices for Navigation Context

Do

  • Use for temporary flow context (return URLs, wizard state, search filters)
  • Use custom scopes for multiple concurrent flows in the same tab
  • Always use type safety with TypeScript generics
  • Trust automatic cleanup - no need to manually clear contexts in ngOnDestroy
  • Check for null when restoring contexts (they may not exist)

Don't

  • Don't store large objects - keep contexts lean (URLs, IDs, simple flags)
  • Don't use for persistent data - use NgRx or services for long-lived state
  • Don't store sensitive data - contexts may be visible in browser dev tools
  • Don't manually clear in ngOnDestroy - tab lifecycle handles cleanup automatically
  • Don't use for cross-tab communication - use services or BroadcastChannel

Cleanup Behavior

Automatic Cleanup (Recommended):

// ✅ No manual cleanup needed - tab lifecycle handles it!
export class CustomerFlowComponent {
  navState = inject(NavigationStateService);

  async startFlow() {
    this.navState.preserveContext({ returnUrl: '/home' });
    // Context automatically cleaned up when tab closes
  }

  // No ngOnDestroy needed!
}

Manual Cleanup (Rarely Needed):

// Use only if you need to explicitly clear contexts during tab lifecycle
export class ComplexFlowComponent {
  navState = inject(NavigationStateService);

  async cancelFlow() {
    // Explicitly clear all contexts for this tab
    const cleared = this.navState.clearScopeContexts();
    console.log(`Cleared ${cleared} contexts`);
  }
}
  • Full API Reference: See libs/core/navigation/README.md for complete documentation
  • Usage Patterns: Detailed examples and patterns in the library README
  • Testing Guide: Full testing guide included in the library documentation
  • Migration Guide: Instructions for migrating from SessionStorage approach in the library README