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:

Actions:

2. ProjectDocsState

File: app/state/project-docs.state.ts
Purpose: Caches documentation structure per project
Data:


๐Ÿ—๏ธ Migration Strategy

Phase 1: Non-Breaking Preparation (Week 1)

Goal: Set up autoFlashState alongside NGXS without removing NGXS

  1. Create new autoFlashState implementations
  2. Run both systems in parallel
  3. Verify data synchronization
  4. Test thoroughly

Phase 2: Gradual Migration (Week 2)

Goal: Switch consumers to autoFlashState one component at a time

  1. Migrate ProjectSelection to autoFlashState
  2. Update all consumers
  3. Verify no regressions
  4. Migrate ProjectDocs

Phase 3: Cleanup (Week 3)

Goal: Remove NGXS completely

  1. Remove NGXS imports
  2. Remove NGXS configuration
  3. Uninstall NGXS packages
  4. 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

  1. Verify persistence: Refresh page, state should restore
  2. Cross-tab sync: Open multiple tabs, verify state syncs
  3. Performance: Measure read/write times (should be <1ms)
  4. IndexedDB: Verify data stored correctly

๐Ÿ“ฆ Rollback Plan

If issues arise during migration:

Quick Rollback (Phase 1-2)

  1. Comment out autoFlashState imports
  2. Uncomment NGXS imports
  3. Restart dev server

Full Rollback (Phase 3)

  1. Restore app.config.ts from git
  2. Restore state files from git
  3. Reinstall NGXS: yarn add @ngxs/store @ngxs/storage-plugin
  4. Restart dev server

๐Ÿ“ˆ Expected Benefits

Performance

Bundle Size

Developer Experience

Maintainability


๐Ÿ“ Migration Checklist

Phase 1: Preparation

Phase 2: Migration

Phase 3: Cleanup


๐ŸŽฏ 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


โœ… Success Criteria

Migration is considered successful when:

  1. โœ… All functionality works identically to NGXS version
  2. โœ… State persists across page reloads
  3. โœ… No console errors or warnings
  4. โœ… Performance metrics improved (faster reads)
  5. โœ… Bundle size reduced (~50KB)
  6. โœ… All tests passing
  7. โœ… Works in all browsers
  8. โœ… Zero NGXS dependencies remaining

Status: ๐Ÿ“‹ Ready for Implementation
Priority: Medium (Non-breaking improvement)
Estimated Effort: 1-2 weeks
Risk Level: Low (Can rollback easily)