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