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:
selectedVehicleresets when switching screensshowAddVehicleform 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
- Create new navigation and user stores
- Implement form state management hook
- Test stores in isolation
Phase 2: Mobile App Integration
- Update App.tsx to use new navigation store
- Modify mobile screens to use form state hook
- Test mobile navigation and persistence
Phase 3: System Integration
- Integrate with existing Auth0 provider
- Update unit system to use new user store
- Ensure backward compatibility
Phase 4: Enhancement & Optimization
- Add advanced features like offline persistence
- Optimize performance and storage usage
- Add error handling and recovery mechanisms
Success Criteria
Upon completion:
- Navigation Consistency: Mobile navigation maintains context across all transitions
- State Persistence: All user data, preferences, and form states persist appropriately
- Form Recovery: Users can navigate away from forms and return without data loss
- User Context: User preferences and settings sync immediately across all screens
- Back Navigation: Mobile back button works correctly with navigation history
- 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.