Aller au contenu principal

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:

  1. context.ts - Creates the React Context
  2. DataContext.tsx - Implements the Context Provider with state logic
  3. useData.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 of useContext(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 database
  • data - 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 query
  • isRegexSearch - Whether to treat search as regular expression
  • sortConfig - Current sort field and direction

Configuration State:

  • flagTypes - Available flag types for categorization
  • uniqueFields - Fields that should be treated as unique identifiers
  • imageField - Field containing image URLs
  • displayFields - 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:

  1. Initializes IndexedDB - Sets up the local database
  2. Loads stored data - Retrieves previously saved JSON objects
  3. Loads configuration - Restores user preferences and flag types
  4. Updates state - Populates all state variables with loaded data
  5. Handles errors - Logs any initialization failures
  6. 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:

  1. Set to true before async operations
  2. Set to false in finally blocks (ensures it's always reset)
  3. 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