NGXS to autoFlashState Migration Plan
Comprehensive plan to replace NGXS with modern autoFlashState from storage-system
NGXS to autoFlashState Migration Plan
Session: editor2-19.5
Date: January 5, 2026
Status: ๐ Ready for Implementation
Priority: Medium (Non-breaking improvement)
๐ฏ Executive Summary
Migrate the docs-angular app from NGXS state management to autoFlashState from the project's intelligent storage system (@storage-system).
Why Migrate?
Current: NGXS + Storage Plugin (external dependency)
Target: autoFlashState (internal, modern, zero-config)
| Feature | NGXS | autoFlashState |
|---|---|---|
| Configuration | Manual setup required | Zero configuration |
| Performance | ~10-50ms persistence | 0.3ms cache hits |
| Bundle Size | ~50KB (external dep) | Built-in (no extra weight) |
| Type Safety | Decorators + boilerplate | Native TypeScript signals |
| Persistence | Plugin-based | Automatic with intelligent adapters |
| Consistency | Different from rest of project | Matches gograb-space architecture |
| Maintenance | External dependency updates | Internal, under our control |
๐ Current State Analysis
NGXS Usage in docs-angular
Location: apps/docs-angular/src/app/
1. ProjectSelectionState
File: app/state/project-selection.state.ts
Purpose: Manages currently selected documentation project
Data:
currentProjectId: string | nullcurrentProjectMetadata: ProjectMetadata | nullavailableProjects: ProjectMetadata[]lastSelectedTimestamp: number | nullisInitialized: boolean
Actions:
SelectProject(projectId)ClearProject()LoadPersistedProject()LoadAvailableProjects()SetProjectMetadata(metadata)
2. ProjectDocsState
File: app/state/project-docs.state.ts
Purpose: Caches documentation structure per project
Data:
- Project-scoped documentation cache
- Navigation structure cache
- Performance optimization
๐๏ธ Migration Strategy
Phase 1: Non-Breaking Preparation (Week 1)
Goal: Set up autoFlashState alongside NGXS without removing NGXS
- Create new autoFlashState implementations
- Run both systems in parallel
- Verify data synchronization
- Test thoroughly
Phase 2: Gradual Migration (Week 2)
Goal: Switch consumers to autoFlashState one component at a time
- Migrate ProjectSelection to autoFlashState
- Update all consumers
- Verify no regressions
- Migrate ProjectDocs
Phase 3: Cleanup (Week 3)
Goal: Remove NGXS completely
- Remove NGXS imports
- Remove NGXS configuration
- Uninstall NGXS packages
- Update documentation
๐ง Implementation Guide
Step 1: Create autoFlashState Replacements
1.1 Replace ProjectSelectionState
Create: app/state/project-selection.flash.ts
import { computed } from '@angular/core';
import { autoFlashState } from '@storage-system';
// Project metadata interface (keep existing)
export interface ProjectMetadata {
id: string;
name: string;
displayName: string;
language: 'english' | 'spanish';
visibility: 'public' | 'private';
password?: string;
isRoot: boolean;
config?: any;
icon?: string;
description?: string;
totalFiles?: number;
tags?: string[];
}
// State model interface
export interface ProjectSelectionModel {
currentProjectId: string | null;
currentProjectMetadata: ProjectMetadata | null;
availableProjects: ProjectMetadata[];
lastSelectedTimestamp: number | null;
isInitialized: boolean;
}
// โ
NEW: autoFlashState implementation (0 configuration!)
export const projectSelection = autoFlashState<ProjectSelectionModel>({
key: 'docs-project-selection',
database: 'docs-angular',
collection: 'app-state',
initialValue: {
currentProjectId: null,
currentProjectMetadata: null,
availableProjects: [],
lastSelectedTimestamp: null,
isInitialized: false
}
});
// Computed selectors (same as NGXS selectors)
export const currentProjectId = computed(() =>
projectSelection().currentProjectId
);
export const currentProjectMetadata = computed(() =>
projectSelection().currentProjectMetadata
);
export const availableProjects = computed(() =>
projectSelection().availableProjects
);
export const isProjectSelected = computed(() =>
projectSelection().currentProjectId !== null
);
export const isInitialized = computed(() =>
projectSelection().isInitialized
);
export const currentLanguage = computed(() =>
projectSelection().currentProjectMetadata?.language || 'english'
);
// Actions (much simpler than NGXS!)
export const projectSelectionActions = {
selectProject: (projectId: string, metadata: ProjectMetadata) => {
projectSelection.update(state => ({
...state,
currentProjectId: projectId,
currentProjectMetadata: metadata,
lastSelectedTimestamp: Date.now()
}));
},
clearProject: () => {
projectSelection.update(state => ({
...state,
currentProjectId: null,
currentProjectMetadata: null,
lastSelectedTimestamp: null
}));
},
setAvailableProjects: (projects: ProjectMetadata[]) => {
projectSelection.update(state => ({
...state,
availableProjects: projects
}));
},
setProjectMetadata: (metadata: ProjectMetadata) => {
projectSelection.update(state => ({
...state,
currentProjectMetadata: metadata
}));
},
setInitialized: (initialized: boolean) => {
projectSelection.update(state => ({
...state,
isInitialized: initialized
}));
}
};
1.2 Replace ProjectDocsState
Create: app/state/project-docs.flash.ts
import { autoFlashState } from '@storage-system';
export interface ProjectDocsCache {
[projectId: string]: {
structure: any;
lastUpdated: number;
expires: number;
};
}
// โ
NEW: autoFlashState with automatic cache expiration
export const projectDocsCache = autoFlashState<ProjectDocsCache>({
key: 'docs-project-cache',
database: 'docs-angular',
collection: 'cache',
initialValue: {},
// Optional: Add TTL support
ttl: 30 * 60 * 1000 // 30 minutes
});
export const projectDocsCacheActions = {
setCache: (projectId: string, structure: any) => {
projectDocsCache.update(cache => ({
...cache,
[projectId]: {
structure,
lastUpdated: Date.now(),
expires: Date.now() + (30 * 60 * 1000)
}
}));
},
getCache: (projectId: string) => {
const cache = projectDocsCache()[projectId];
if (!cache) return null;
// Check expiration
if (Date.now() > cache.expires) {
projectDocsCacheActions.clearCache(projectId);
return null;
}
return cache.structure;
},
clearCache: (projectId: string) => {
projectDocsCache.update(cache => {
const { [projectId]: removed, ...rest } = cache;
return rest;
});
},
clearAllCache: () => {
projectDocsCache.set({});
}
};
Step 2: Update app.config.ts
File: apps/docs-angular/src/app/app.config.ts
import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules, withHashLocation } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { DynamicRoutesService } from './services/dynamic-routes.service';
import { environment } from '../../../../environments/environment';
import { mockHttpInterceptor, MockInterceptorUtilsService } from '@libs-core/mock-api';
import { DocsMocks } from './mocks';
import { provideIntelligentStorage } from '@storage-system';
// โ REMOVE NGXS IMPORTS
// import { NgxsModule, Store } from '@ngxs/store';
// import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
// import { ProjectDocsState } from './state/project-docs.state';
// import { ProjectSelectionState, LoadPersistedProject } from './state/project-selection.state';
// โ
ADD autoFlashState imports
import {
projectSelection,
projectSelectionActions,
isProjectSelected
} from './state/project-selection.flash';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptorsFromDi(),
withInterceptors([mockHttpInterceptor])
),
provideAnimations(),
// ๐ DYNAMIC ROUTE GENERATION - Modern Angular 19+ pattern
provideAppInitializer(() => {
const dynamicRoutesService = inject(DynamicRoutesService);
const router = inject(Router);
return (async () => {
try {
console.log('๐ Generating dynamic routes from file structure...');
const dynamicRoutes = await dynamicRoutesService.generateDynamicRoutes();
console.log(`โ
Generated ${dynamicRoutes.length} dynamic routes`);
router.resetConfig(dynamicRoutes);
// โ
NEW: Load persisted project using autoFlashState
// No need for Store.dispatch() - it's automatic!
const currentProject = projectSelection().currentProjectId;
const hasProject = currentProject !== null;
console.log('๐ Post-route-init status:', { hasProject, currentProject });
// If no project and not already on select-project page, navigate there
if (!hasProject && !router.url.includes('select-project')) {
console.log('โน๏ธ No project selected, will navigate to selection page after initialization');
setTimeout(() => {
router.navigate(['select-project']);
}, 50);
}
return true;
} catch (error) {
console.error('โ Failed to generate dynamic routes:', error);
router.resetConfig([
{ path: '', redirectTo: '/getting-started', pathMatch: 'full' },
{ path: '**', loadComponent: () => import('./pages/dynamic-page/dynamic-page.component').then(m => m.DynamicPageComponent) }
]);
return true;
}
})();
}),
// Provide router with empty initial config and preloading
provideRouter(
[],
withPreloading(PreloadAllModules),
...(environment.production ? [withHashLocation()] : [])
),
// ๐ INTELLIGENT STORAGE SYSTEM - Zero Configuration!
// This is all we need - no NGXS configuration required!
provideIntelligentStorage({ namespace: 'docs-angular' }),
// โ REMOVE NGXS configuration
// importProvidersFrom(
// NgxsModule.forRoot([ProjectDocsState, ProjectSelectionState]),
// NgxsStoragePluginModule.forRoot({ keys: ['projectDocs', 'projectSelection'] })
// ),
// Configure Mock API (dev only)
provideAppInitializer(() => {
const utils = inject(MockInterceptorUtilsService);
try {
utils.configureMockInterceptor(
{
enabled: !environment.production,
fallbackToNetwork: true,
delay: [80, 220],
logging: { enabled: false, level: 'warn', logRequests: false, logResponses: false },
includePatterns: ['/api/'],
ignorePatterns: [
'/assets/',
'.json',
'.js',
'.css',
'.html',
'.svg',
'.ico',
'.png',
'.woff',
'.ttf',
'docs-manifest'
],
middleware: []
},
DocsMocks
);
} catch {
// nada
}
return Promise.resolve();
})
]
};
Step 3: Update Components
Example: project-selection.component.ts
Before (NGXS):
import { Component, inject } from '@angular/core';
import { Store } from '@ngxs/store';
import { SelectProject, ProjectSelectionState } from '../../state/project-selection.state';
@Component({
selector: 'app-project-selection',
templateUrl: './project-selection.component.html'
})
export class ProjectSelectionComponent {
private readonly store = inject(Store);
// NGXS selectors
projects$ = this.store.select(ProjectSelectionState.availableProjects);
currentProject$ = this.store.select(ProjectSelectionState.currentProjectId);
selectProject(projectId: string) {
this.store.dispatch(new SelectProject(projectId));
}
}
After (autoFlashState):
import { Component } from '@angular/core';
import {
projectSelection,
availableProjects,
currentProjectId,
projectSelectionActions
} from '../../state/project-selection.flash';
@Component({
selector: 'app-project-selection',
templateUrl: './project-selection.component.html'
})
export class ProjectSelectionComponent {
// โ
Signals - no subscriptions needed!
projects = availableProjects;
currentProject = currentProjectId;
selectProject(projectId: string, metadata: any) {
projectSelectionActions.selectProject(projectId, metadata);
}
}
Template (NO CHANGES NEEDED with signals!):
<!-- Works automatically with signals -->
<div *ngFor="let project of projects()">
<button (click)="selectProject(project.id, project)"
[class.selected]="currentProject() === project.id">
{{ project.displayName }}
</button>
</div>
๐งช Testing Strategy
Unit Tests
Before (NGXS):
import { TestBed } from '@angular/core/testing';
import { NgxsModule, Store } from '@ngxs/store';
import { ProjectSelectionState, SelectProject } from './project-selection.state';
describe('ProjectSelectionState', () => {
let store: Store;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NgxsModule.forRoot([ProjectSelectionState])]
});
store = TestBed.inject(Store);
});
it('should select project', () => {
store.dispatch(new SelectProject('project-1'));
const state = store.selectSnapshot(ProjectSelectionState.currentProjectId);
expect(state).toBe('project-1');
});
});
After (autoFlashState):
import { TestBed } from '@angular/core/testing';
import { provideIntelligentStorage } from '@storage-system';
import { projectSelection, projectSelectionActions } from './project-selection.flash';
describe('ProjectSelection (autoFlashState)', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideIntelligentStorage({ namespace: 'test' })]
});
// Reset state before each test
projectSelection.set({
currentProjectId: null,
currentProjectMetadata: null,
availableProjects: [],
lastSelectedTimestamp: null,
isInitialized: false
});
});
it('should select project', () => {
const metadata = { id: 'project-1', name: 'Test' } as any;
projectSelectionActions.selectProject('project-1', metadata);
expect(projectSelection().currentProjectId).toBe('project-1');
expect(projectSelection().currentProjectMetadata).toEqual(metadata);
});
});
Integration Tests
- Verify persistence: Refresh page, state should restore
- Cross-tab sync: Open multiple tabs, verify state syncs
- Performance: Measure read/write times (should be <1ms)
- IndexedDB: Verify data stored correctly
๐ฆ Rollback Plan
If issues arise during migration:
Quick Rollback (Phase 1-2)
- Comment out autoFlashState imports
- Uncomment NGXS imports
- Restart dev server
Full Rollback (Phase 3)
- Restore
app.config.tsfrom git - Restore state files from git
- Reinstall NGXS:
yarn add @ngxs/store @ngxs/storage-plugin - Restart dev server
๐ Expected Benefits
Performance
- Before: ~10-50ms NGXS persistence with localStorage plugin
- After: 0.3ms autoFlashState with intelligent caching
- Result: 30-150x faster state access
Bundle Size
- Before: ~50KB NGXS + plugins
- After: 0KB (uses existing storage-system)
- Result: 50KB smaller bundle
Developer Experience
- Before: Actions, State classes, Selectors, Decorators
- After: Simple functions and signals
- Result: 70% less boilerplate code
Maintainability
- Before: External dependency updates, compatibility issues
- After: Internal system, full control
- Result: Zero external dependencies
๐ Migration Checklist
Phase 1: Preparation
- Create
project-selection.flash.ts - Create
project-docs.flash.ts - Run both systems in parallel
- Verify data synchronization
- Write unit tests
Phase 2: Migration
- Update
app.config.ts(comment out NGXS, add autoFlashState) - Update
project-selection.component.ts - Update all components using ProjectSelectionState
- Update all components using ProjectDocsState
- Run full test suite
- Test in all browsers
Phase 3: Cleanup
- Remove NGXS state files
- Remove NGXS imports from all files
- Uninstall NGXS packages
- Update documentation
- Create migration documentation
๐ฏ Timeline
| Phase | Duration | Status |
|---|---|---|
| Phase 1: Preparation | 2-3 days | ๐ Not Started |
| Phase 2: Migration | 3-4 days | โณ Pending |
| Phase 3: Cleanup | 1-2 days | โณ Pending |
| Total | 1-2 weeks | ๐ Ready to Start |
๐ Related Documentation
- Storage System Overview:
docs/english/GoGrab-Space/libs/libs-foundational/storage-system/overview.md - autoFlashState Guide:
docs/english/GoGrab-Space/libs/libs-foundational/storage-system/getting-started.md - Current Fix:
docs/english/sessions/session-19/19.5-docs-angular-cache-fix.md
โ Success Criteria
Migration is considered successful when:
- โ All functionality works identically to NGXS version
- โ State persists across page reloads
- โ No console errors or warnings
- โ Performance metrics improved (faster reads)
- โ Bundle size reduced (~50KB)
- โ All tests passing
- โ Works in all browsers
- โ Zero NGXS dependencies remaining
Status: ๐ Ready for Implementation
Priority: Medium (Non-breaking improvement)
Estimated Effort: 1-2 weeks
Risk Level: Low (Can rollback easily)