State Management System
Overview
The Dynamic JSON Visualizer uses React Context to manage application state centrally. This system provides a single source of truth for all data, UI settings, and user preferences, making them accessible to any component in the application without prop drilling.
Architecture
The state management consists of three key files:
context.ts
- Creates the React ContextDataContext.tsx
- Implements the Context Provider with state logicuseData.ts
- Custom hook for consuming the context
Context Creation (context.ts
)
import { createContext } from 'react';
import type { DataContextType } from '../types';
export const DataContext = createContext<DataContextType | undefined>(undefined);
This creates a React Context with TypeScript typing. The context can either contain a DataContextType
object or be undefined
(when used outside a provider).
Custom Hook (useData.ts
)
export const useData = (): DataContextType => {
const context = useContext(DataContext);
if (context === undefined) {
throw new Error('useData must be used within a DataProvider');
}
return context;
};
This custom hook provides a safe way to access the context:
- Error checking: Throws an error if used outside the DataProvider
- Type safety: Returns the properly typed context value
- Convenience: Components can simply call
useData()
instead ofuseContext(DataContext)
State Provider (DataContext.tsx
)
The DataProvider
component manages all application state using React hooks and provides it to child components.
State Variables
The provider maintains several pieces of state using useState
:
const [originalData, setOriginalData] = useState<JSONObject[]>([]);
const [data, setData] = useState<JSONObject[]>([]);
const [loading, setLoading] = useState(true);
const [flagTypes, setFlagTypes] = useState<FlagType[]>([]);
const [viewMode, setViewMode] = useState<ViewMode>('card');
const [searchTerm, setSearchTerm] = useState('');
const [isRegexSearch, setIsRegexSearch] = useState(false);
const [sortConfig, setSortConfig] = useState<SortConfig | null>(null);
const [uniqueFields, setUniqueFields] = useState<string[]>([]);
const [imageField, setImageField] = useState<string | null>(null);
const [displayFields, setDisplayFields] = useState<string[]>([]);
Data State:
originalData
- The unfiltered, unsorted source data from the databasedata
- The currently displayed data (filtered and sorted)loading
- Boolean indicating if async operations are in progress
UI State:
viewMode
- Current view ('card' or 'table')searchTerm
- Current search queryisRegexSearch
- Whether to treat search as regular expressionsortConfig
- Current sort field and direction
Configuration State:
flagTypes
- Available flag types for categorizationuniqueFields
- Fields that should be treated as unique identifiersimageField
- Field containing image URLsdisplayFields
- Fields to show in the interface
Initialization Effect
useEffect(() => {
const init = async () => {
setLoading(true);
try {
await initDB();
const storedObjects = await getAllJsonObjects();
const storedFlagTypes = await getAllFlagTypes();
const config = await getAppConfig();
setOriginalData(storedObjects);
setData(storedObjects);
setFlagTypes(storedFlagTypes);
setUniqueFields(config.uniqueFields);
setImageField(config.imageField);
setDisplayFields(config.displayFields);
} catch (error) {
console.error('Failed to initialize the database:', error);
} finally {
setLoading(false);
}
};
init();
}, []);
This effect runs once when the component mounts and:
- Initializes IndexedDB - Sets up the local database
- Loads stored data - Retrieves previously saved JSON objects
- Loads configuration - Restores user preferences and flag types
- Updates state - Populates all state variables with loaded data
- Handles errors - Logs any initialization failures
- Manages loading - Sets loading to false when complete
Data Filtering and Sorting Effect
useEffect(() => {
let filteredData = originalData;
if (searchTerm) {
filteredData = filterData(filteredData, searchTerm, isRegexSearch);
}
if (sortConfig) {
filteredData = sortData(filteredData, sortConfig.key, sortConfig.direction);
}
setData(filteredData);
}, [originalData, searchTerm, isRegexSearch, sortConfig]);
This effect automatically updates the displayed data whenever:
- The original data changes (new uploads, deletions)
- The search term changes
- The search mode changes (text vs regex)
- The sort configuration changes
The effect applies transformations in order: filter first, then sort.
State Management Functions
The provider implements several functions for data manipulation:
Data Operations
loadJsonData
- Uploads new JSON data:
const loadJsonData = async (jsonData: JSONObject[], fields?: string[]) => {
setLoading(true);
try {
const result = await storeJsonObjects(jsonData, fields ?? uniqueFields);
const updatedData = await getAllJsonObjects();
setOriginalData(updatedData);
return result;
} finally {
setLoading(false);
}
};
deleteObjects
- Removes selected objects:
const deleteObjects = async (ids: string[]) => {
setLoading(true);
try {
await deleteObjectsFromDb(ids);
const updatedData = await getAllJsonObjects();
setOriginalData(updatedData);
toast.success(`Successfully deleted ${ids.length} objects`);
} finally {
setLoading(false);
}
};
Flag Management
updateFlag
- Toggles flags on individual objects:
const updateFlag = async (id: string, flagId: string, value: boolean) => {
setLoading(true);
try {
await updateObjectFlag(id, flagId, value);
const updatedData = await getAllJsonObjects();
setOriginalData(updatedData);
} finally {
setLoading(false);
}
};
addFlagType
- Creates new flag types:
const addFlagType = async (name: string, color: string, icon: string) => {
try {
const newFlag = await addFlagTypeToDb(name, color, icon);
setFlagTypes(prev => [...prev, newFlag]);
} catch (error) {
console.error('Failed to add flag type:', error);
}
};
Key Design Patterns
Optimistic Updates vs Database Sync
The state management uses two different update patterns:
Database Sync Pattern (for data operations):
await updateObjectFlag(id, flagId, value);
const updatedData = await getAllJsonObjects();
setOriginalData(updatedData);
- Updates the database first
- Reloads all data from database
- Ensures consistency but slower
Optimistic Update Pattern (for flag types):
const newFlag = await addFlagTypeToDb(name, color, icon);
setFlagTypes(prev => [...prev, newFlag]);
- Updates local state immediately
- Faster user experience
- Used for operations less likely to fail
Error Handling
All async operations include try-catch blocks:
try {
// Database operation
} catch (error) {
console.error('Operation failed:', error);
toast.error('User-friendly error message');
} finally {
setLoading(false);
}
Loading State Management
The loading
state is consistently managed:
- Set to
true
before async operations - Set to
false
infinally
blocks (ensures it's always reset) - Used by UI components to show loading indicators
Usage in Components
Components access the state using the custom hook:
import { useData } from '../contexts/useData';
const MyComponent = () => {
const { data, loading, searchTerm, setSearchTerm } = useData();
if (loading) return <div>Loading...</div>;
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{data.map(item => <div key={item.__id}>{/* render item */}</div>)}
</div>
);
};
This pattern provides:
- Type safety - All properties are properly typed
- Reactivity - Components re-render when state changes
- Centralization - All state logic is in one place
- Testability - State logic can be tested independently