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

357 lines
12 KiB
Markdown

# 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`:
```typescript
// 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)
```typescript
// 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
```typescript
// 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
```typescript
// 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:
```typescript
// 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):**
```typescript
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):**
```typescript
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):**
```typescript
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:**
```html
@if (hasReturnUrl()) {
<button (click)="complete()">Zurück</button>
}
```
### Advanced: Multiple Concurrent Flows
Use custom scopes to manage multiple flows in the same tab:
```typescript
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):**
```typescript
// ✅ 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):**
```typescript
// 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`);
}
}
```
### Related Documentation
- **Full API Reference**: See [libs/core/navigation/README.md](../../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