Files
motovaultpro/docs/changes/mobile-optimization-v1/04-STATE-MANAGEMENT.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

18 KiB

State Management & Navigation Consistency Solutions

Overview

This document addresses critical state management issues in mobile navigation, including context loss during screen transitions, form state persistence, and navigation consistency between mobile and desktop platforms.

Issues Identified

1. Mobile State Reset Issues

Location: frontend/src/App.tsx mobile navigation logic

Problem: Navigation between screens resets critical state:

  • selectedVehicle resets when switching screens
  • showAddVehicle form state lost during navigation
  • User context not maintained across screen transitions
  • Mobile navigation doesn't preserve history

2. Navigation Paradigm Split

Mobile: State-based navigation without URLs (activeScreen state) Desktop: URL-based routing with React Router Impact: Inconsistent user experience and different development patterns

3. State Persistence Gaps

  • User context not persisted (requires re-authentication overhead)
  • Form data lost when navigating away
  • Mobile navigation state not preserved across app restarts
  • Settings changes not immediately reflected across screens

Solution Architecture

Enhanced Mobile State Management

1. Navigation State Persistence

File: frontend/src/core/store/navigation.ts (new)

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export type MobileScreen = 'dashboard' | 'vehicles' | 'fuel' | 'settings';
export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit';

interface NavigationState {
  // Current navigation state
  activeScreen: MobileScreen;
  vehicleSubScreen: VehicleSubScreen;
  selectedVehicleId: string | null;

  // Navigation history for back button
  navigationHistory: {
    screen: MobileScreen;
    vehicleSubScreen?: VehicleSubScreen;
    selectedVehicleId?: string | null;
    timestamp: number;
  }[];

  // Form state preservation
  formStates: Record<string, any>;

  // Actions
  navigateToScreen: (screen: MobileScreen) => void;
  navigateToVehicleSubScreen: (subScreen: VehicleSubScreen, vehicleId?: string) => void;
  goBack: () => void;
  saveFormState: (formId: string, state: any) => void;
  restoreFormState: (formId: string) => any;
  clearFormState: (formId: string) => void;
}

export const useNavigationStore = create<NavigationState>()(
  persist(
    (set, get) => ({
      // Initial state
      activeScreen: 'vehicles',
      vehicleSubScreen: 'list',
      selectedVehicleId: null,
      navigationHistory: [],
      formStates: {},

      // Navigation actions
      navigateToScreen: (screen) => {
        const currentState = get();
        const historyEntry = {
          screen: currentState.activeScreen,
          vehicleSubScreen: currentState.vehicleSubScreen,
          selectedVehicleId: currentState.selectedVehicleId,
          timestamp: Date.now(),
        };

        set({
          activeScreen: screen,
          vehicleSubScreen: screen === 'vehicles' ? 'list' : currentState.vehicleSubScreen,
          selectedVehicleId: screen === 'vehicles' ? currentState.selectedVehicleId : null,
          navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10), // Keep last 10
        });
      },

      navigateToVehicleSubScreen: (subScreen, vehicleId = null) => {
        const currentState = get();
        const historyEntry = {
          screen: currentState.activeScreen,
          vehicleSubScreen: currentState.vehicleSubScreen,
          selectedVehicleId: currentState.selectedVehicleId,
          timestamp: Date.now(),
        };

        set({
          vehicleSubScreen: subScreen,
          selectedVehicleId: vehicleId || currentState.selectedVehicleId,
          navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10),
        });
      },

      goBack: () => {
        const currentState = get();
        const lastEntry = currentState.navigationHistory[currentState.navigationHistory.length - 1];

        if (lastEntry) {
          set({
            activeScreen: lastEntry.screen,
            vehicleSubScreen: lastEntry.vehicleSubScreen || 'list',
            selectedVehicleId: lastEntry.selectedVehicleId,
            navigationHistory: currentState.navigationHistory.slice(0, -1),
          });
        }
      },

      // Form state management
      saveFormState: (formId, state) => {
        set((current) => ({
          formStates: {
            ...current.formStates,
            [formId]: { ...state, timestamp: Date.now() },
          },
        }));
      },

      restoreFormState: (formId) => {
        const state = get().formStates[formId];
        // Return state if it's less than 1 hour old
        if (state && Date.now() - state.timestamp < 3600000) {
          return state;
        }
        return null;
      },

      clearFormState: (formId) => {
        set((current) => {
          const newFormStates = { ...current.formStates };
          delete newFormStates[formId];
          return { formStates: newFormStates };
        });
      },
    }),
    {
      name: 'motovaultpro-mobile-navigation',
      partialize: (state) => ({
        activeScreen: state.activeScreen,
        vehicleSubScreen: state.vehicleSubScreen,
        selectedVehicleId: state.selectedVehicleId,
        formStates: state.formStates,
        // Don't persist navigation history - rebuild on app start
      }),
    }
  )
);

2. Enhanced User Context Persistence

File: frontend/src/core/store/user.ts (new)

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface UserPreferences {
  unitSystem: 'imperial' | 'metric';
  darkMode: boolean;
  notifications: {
    email: boolean;
    push: boolean;
    maintenance: boolean;
  };
}

interface UserState {
  // User data (persisted subset)
  userProfile: {
    id: string;
    name: string;
    email: string;
    picture: string;
  } | null;

  preferences: UserPreferences;

  // Session data (not persisted)
  isOnline: boolean;
  lastSyncTimestamp: number;

  // Actions
  setUserProfile: (profile: any) => void;
  updatePreferences: (preferences: Partial<UserPreferences>) => void;
  setOnlineStatus: (isOnline: boolean) => void;
  updateLastSync: () => void;
  clearUserData: () => void;
}

export const useUserStore = create<UserState>()(
  persist(
    (set) => ({
      // Initial state
      userProfile: null,
      preferences: {
        unitSystem: 'imperial',
        darkMode: false,
        notifications: {
          email: true,
          push: true,
          maintenance: true,
        },
      },
      isOnline: true,
      lastSyncTimestamp: 0,

      // Actions
      setUserProfile: (profile) => {
        if (profile) {
          set({
            userProfile: {
              id: profile.sub,
              name: profile.name,
              email: profile.email,
              picture: profile.picture,
            },
          });
        }
      },

      updatePreferences: (newPreferences) => {
        set((state) => ({
          preferences: { ...state.preferences, ...newPreferences },
        }));
      },

      setOnlineStatus: (isOnline) => set({ isOnline }),

      updateLastSync: () => set({ lastSyncTimestamp: Date.now() }),

      clearUserData: () => set({
        userProfile: null,
        preferences: {
          unitSystem: 'imperial',
          darkMode: false,
          notifications: {
            email: true,
            push: true,
            maintenance: true,
          },
        },
      }),
    }),
    {
      name: 'motovaultpro-user-context',
      partialize: (state) => ({
        userProfile: state.userProfile,
        preferences: state.preferences,
        // Don't persist session data
      }),
    }
  )
);

3. Smart Form State Hook

File: frontend/src/core/hooks/useFormState.ts (new)

import { useState, useEffect, useCallback } from 'react';
import { useNavigationStore } from '../store/navigation';

export interface UseFormStateOptions {
  formId: string;
  defaultValues: Record<string, any>;
  autoSave?: boolean;
  saveDelay?: number;
}

export const useFormState = <T extends Record<string, any>>({
  formId,
  defaultValues,
  autoSave = true,
  saveDelay = 1000,
}: UseFormStateOptions) => {
  const { saveFormState, restoreFormState, clearFormState } = useNavigationStore();
  const [formData, setFormData] = useState<T>(defaultValues as T);
  const [hasChanges, setHasChanges] = useState(false);
  const [isRestored, setIsRestored] = useState(false);

  // Restore form state on mount
  useEffect(() => {
    const restoredState = restoreFormState(formId);
    if (restoredState && !isRestored) {
      setFormData({ ...defaultValues, ...restoredState });
      setHasChanges(true);
      setIsRestored(true);
    }
  }, [formId, restoreFormState, defaultValues, isRestored]);

  // Auto-save with debounce
  useEffect(() => {
    if (!autoSave || !hasChanges) return;

    const timer = setTimeout(() => {
      saveFormState(formId, formData);
    }, saveDelay);

    return () => clearTimeout(timer);
  }, [formData, hasChanges, autoSave, saveDelay, formId, saveFormState]);

  const updateFormData = useCallback((updates: Partial<T>) => {
    setFormData((current) => ({ ...current, ...updates }));
    setHasChanges(true);
  }, []);

  const resetForm = useCallback(() => {
    setFormData(defaultValues as T);
    setHasChanges(false);
    clearFormState(formId);
  }, [defaultValues, formId, clearFormState]);

  const submitForm = useCallback(() => {
    setHasChanges(false);
    clearFormState(formId);
  }, [formId, clearFormState]);

  return {
    formData,
    updateFormData,
    resetForm,
    submitForm,
    hasChanges,
    isRestored,
  };
};

Implementation in App.tsx

Updated Mobile Navigation Logic

File: frontend/src/App.tsx (modifications)

import { useNavigationStore } from './core/store/navigation';
import { useUserStore } from './core/store/user';

// Replace existing mobile detection and state management
const MobileApp: React.FC = () => {
  const { user, isAuthenticated } = useAuth0();
  const {
    activeScreen,
    vehicleSubScreen,
    selectedVehicleId,
    navigateToScreen,
    navigateToVehicleSubScreen,
    goBack,
  } = useNavigationStore();

  const { setUserProfile } = useUserStore();

  // Update user profile when authenticated
  useEffect(() => {
    if (isAuthenticated && user) {
      setUserProfile(user);
    }
  }, [isAuthenticated, user, setUserProfile]);

  // Handle mobile back button
  useEffect(() => {
    const handlePopState = (event: PopStateEvent) => {
      event.preventDefault();
      goBack();
    };

    window.addEventListener('popstate', handlePopState);
    return () => window.removeEventListener('popstate', handlePopState);
  }, [goBack]);

  const handleVehicleSelect = (vehicleId: string) => {
    navigateToVehicleSubScreen('detail', vehicleId);
  };

  const handleAddVehicle = () => {
    navigateToVehicleSubScreen('add');
  };

  const handleBackToList = () => {
    navigateToVehicleSubScreen('list');
  };

  // Render screens based on navigation state
  const renderActiveScreen = () => {
    switch (activeScreen) {
      case 'vehicles':
        return renderVehiclesScreen();
      case 'fuel':
        return <FuelScreen />;
      case 'dashboard':
        return <DashboardScreen />;
      case 'settings':
        return <MobileSettingsScreen />;
      default:
        return renderVehiclesScreen();
    }
  };

  const renderVehiclesScreen = () => {
    switch (vehicleSubScreen) {
      case 'list':
        return (
          <VehiclesMobileScreen
            onVehicleSelect={handleVehicleSelect}
            onAddVehicle={handleAddVehicle}
          />
        );
      case 'detail':
        return (
          <VehicleDetailMobile
            vehicleId={selectedVehicleId!}
            onBack={handleBackToList}
          />
        );
      case 'add':
        return (
          <AddVehicleScreen
            onBack={handleBackToList}
            onVehicleAdded={handleBackToList}
          />
        );
      default:
        return (
          <VehiclesMobileScreen
            onVehicleSelect={handleVehicleSelect}
            onAddVehicle={handleAddVehicle}
          />
        );
    }
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
      {renderActiveScreen()}

      <BottomNavigation
        activeScreen={activeScreen}
        onScreenChange={navigateToScreen}
      />
    </div>
  );
};

Enhanced Add Vehicle Form with State Persistence

File: frontend/src/features/vehicles/mobile/AddVehicleScreen.tsx (example usage)

import React from 'react';
import { useFormState } from '../../../core/hooks/useFormState';

interface AddVehicleScreenProps {
  onBack: () => void;
  onVehicleAdded: () => void;
}

export const AddVehicleScreen: React.FC<AddVehicleScreenProps> = ({
  onBack,
  onVehicleAdded,
}) => {
  const {
    formData,
    updateFormData,
    resetForm,
    submitForm,
    hasChanges,
    isRestored,
  } = useFormState({
    formId: 'add-vehicle',
    defaultValues: {
      year: '',
      make: '',
      model: '',
      trim: '',
      vin: '',
      licensePlate: '',
      nickname: '',
    },
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      // Submit vehicle data
      await submitVehicle(formData);
      submitForm(); // Clear saved state
      onVehicleAdded();
    } catch (error) {
      // Handle error, form state is preserved
      console.error('Error adding vehicle:', error);
    }
  };

  return (
    <div className="p-4">
      <div className="flex items-center mb-6">
        <button onClick={onBack} className="mr-4">
          <ArrowLeft className="w-6 h-6" />
        </button>
        <h1 className="text-xl font-bold">Add Vehicle</h1>
        {isRestored && (
          <span className="ml-auto text-sm text-blue-600">Draft restored</span>
        )}
      </div>

      <form onSubmit={handleSubmit} className="space-y-4">
        <input
          type="text"
          placeholder="Year"
          value={formData.year}
          onChange={(e) => updateFormData({ year: e.target.value })}
          className="w-full p-3 border rounded-lg"
        />

        {/* More form fields... */}

        <div className="flex space-x-3">
          <button
            type="button"
            onClick={resetForm}
            className="flex-1 py-3 bg-gray-200 text-gray-700 rounded-lg"
          >
            Clear
          </button>
          <button
            type="submit"
            className="flex-1 py-3 bg-blue-600 text-white rounded-lg"
          >
            Add Vehicle
          </button>
        </div>

        {hasChanges && (
          <p className="text-sm text-blue-600 text-center">
            Changes are being saved automatically
          </p>
        )}
      </form>
    </div>
  );
};

Integration with Existing Systems

1. Zustand Store Integration

File: frontend/src/core/store/index.ts (existing file modifications)

// Export new stores alongside existing ones
export { useNavigationStore } from './navigation';
export { useUserStore } from './user';

// Keep existing store exports
export { useAppStore } from './app';

2. Auth0 Integration Enhancement

File: frontend/src/core/auth/Auth0Provider.tsx (modifications)

import { useUserStore } from '../store/user';

// Inside the Auth0Provider component
const { setUserProfile, clearUserData } = useUserStore();

// Update user profile on authentication
useEffect(() => {
  if (isAuthenticated && user) {
    setUserProfile(user);
  } else if (!isAuthenticated) {
    clearUserData();
  }
}, [isAuthenticated, user, setUserProfile, clearUserData]);

3. Unit System Integration

File: frontend/src/shared-minimal/utils/units.ts (modifications)

import { useUserStore } from '../../core/store/user';

// Update existing unit hooks to use new store
export const useUnitSystem = () => {
  const { preferences, updatePreferences } = useUserStore();

  const toggleUnitSystem = () => {
    const newSystem = preferences.unitSystem === 'imperial' ? 'metric' : 'imperial';
    updatePreferences({ unitSystem: newSystem });
  };

  return {
    unitSystem: preferences.unitSystem,
    toggleUnitSystem,
  };
};

Testing Requirements

State Persistence Tests

  • Navigation state persists across app restarts
  • Selected vehicle context maintained during navigation
  • Form state preserved when navigating away and returning
  • User preferences persist and sync across screens
  • Navigation history works correctly with back button

Mobile Navigation Tests

  • Screen transitions maintain context
  • Bottom navigation reflects current state accurately
  • Add vehicle form preserves data during interruptions
  • Settings changes reflect immediately across screens
  • Authentication state managed correctly

Integration Tests

  • New stores integrate properly with existing components
  • Auth0 integration works with enhanced user persistence
  • Unit system changes sync between old and new systems
  • No conflicts with existing Zustand store patterns

Migration Strategy

Phase 1: Store Creation

  1. Create new navigation and user stores
  2. Implement form state management hook
  3. Test stores in isolation

Phase 2: Mobile App Integration

  1. Update App.tsx to use new navigation store
  2. Modify mobile screens to use form state hook
  3. Test mobile navigation and persistence

Phase 3: System Integration

  1. Integrate with existing Auth0 provider
  2. Update unit system to use new user store
  3. Ensure backward compatibility

Phase 4: Enhancement & Optimization

  1. Add advanced features like offline persistence
  2. Optimize performance and storage usage
  3. Add error handling and recovery mechanisms

Success Criteria

Upon completion:

  1. Navigation Consistency: Mobile navigation maintains context across all transitions
  2. State Persistence: All user data, preferences, and form states persist appropriately
  3. Form Recovery: Users can navigate away from forms and return without data loss
  4. User Context: User preferences and settings sync immediately across all screens
  5. Back Navigation: Mobile back button works correctly with navigation history
  6. Integration: New state management integrates seamlessly with existing systems

This enhanced state management system will provide a robust foundation for consistent mobile and desktop experiences while maintaining all existing functionality.