Skip to main content
Glama
useStateManager.ts9.06 kB
/** * React hooks for optimized state management integration * Provides memoized, performant access to the centralized state manager */ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { stateManager, StateListener } from '../core/stateManager'; import { logger } from '../core/logger'; // Basic state subscription hook with memoization export function useStateValue<T = any>(path: string, defaultValue?: T): [T, (value: T) => void] { const [, forceUpdate] = useState({}); const currentValueRef = useRef<T>(defaultValue as T); const listenerRef = useRef<StateListener<T>>(); const pathRef = useRef(path); // Update refs when path changes if (pathRef.current !== path) { pathRef.current = path; currentValueRef.current = stateManager.getValue<T>(path) ?? (defaultValue as T); } const setValue = useCallback((newValue: T) => { stateManager.setValue(path, newValue); }, [path]); // Memoize current value to prevent unnecessary re-renders const currentValue = useMemo(() => { const value = stateManager.getValue<T>(path); return value !== undefined ? value : (defaultValue as T); }, [path, defaultValue]); // Update ref and trigger re-render if value changed if (currentValueRef.current !== currentValue) { currentValueRef.current = currentValue; } useEffect(() => { const listener: StateListener<T> = (newValue) => { if (currentValueRef.current !== newValue) { currentValueRef.current = newValue; forceUpdate({}); } }; // Initial subscription const unsubscribe = stateManager.subscribe(path, listener); listenerRef.current = listener; // Get current value const initialValue = stateManager.getValue<T>(path); if (initialValue !== undefined && currentValueRef.current !== initialValue) { currentValueRef.current = initialValue; forceUpdate({}); } return unsubscribe; }, [path]); return [currentValueRef.current, setValue]; } // Optimized hook for accessing nested state paths export function useNestedState<T = any>(basePath: string, paths: string[]): Record<string, T> { const [, forceUpdate] = useState({}); const valuesRef = useRef<Record<string, T>>({} as Record<string, T>); const listenersRef = useRef<Map<string, () => void>>(new Map()); const fullPaths = useMemo(() => { return paths.map(path => `${basePath}.${path}`); }, [basePath, paths]); // Memoize current values const currentValues = useMemo(() => { const values: Record<string, T> = {}; fullPaths.forEach((fullPath, index) => { const value = stateManager.getValue<T>(fullPath); values[paths[index]] = value; }); return values; }, [fullPaths, paths]); // Check if values changed const valuesChanged = useMemo(() => { return !Object.keys(currentValues).every(key => valuesRef.current[key] === currentValues[key] ); }, [currentValues]); // Update refs and trigger re-render if needed if (valuesChanged) { valuesRef.current = { ...currentValues }; } useEffect(() => { // Clean up old listeners listenersRef.current.forEach(unsubscribe => unsubscribe()); listenersRef.current.clear(); // Create new listeners for each path fullPaths.forEach((fullPath, index) => { const unsubscribe = stateManager.subscribe(fullPath, (newValue) => { const key = paths[index]; if (valuesRef.current[key] !== newValue) { valuesRef.current[key] = newValue; forceUpdate({}); } }); listenersRef.current.set(fullPath, unsubscribe); }); return () => { listenersRef.current.forEach(unsubscribe => unsubscribe()); listenersRef.current.clear(); }; }, [fullPaths, paths]); return valuesRef.current; } // Hook for state persistence with debounce export function usePersistedState<T = any>( path: string, defaultValue: T, debounceMs: number = 500 ): [T, (value: T) => void] { const [value, setValue] = useStateValue<T>(path, defaultValue); const timeoutRef = useRef<number>(); const setPersistedValue = useCallback((newValue: T) => { setValue(newValue); // Debounce persistence if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = window.setTimeout(() => { stateManager.setValue(path, newValue); logger.debug('use-persisted-state', `State persisted: ${path}`, { value: newValue }); }, debounceMs); }, [path, setValue, debounceMs]); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); return [value, setPersistedValue]; } // Hook for computed state with memoization export function useComputedState<T = any>( dependencies: string[], computeFn: () => T ): T { const [computedValue, setComputedValue] = useState<T>(() => computeFn()); const dependenciesRef = useRef<string[]>(dependencies); const computeFnRef = useRef(computeFn); // Update refs when dependencies or compute function change dependenciesRef.current = dependencies; computeFnRef.current = computeFn; useEffect(() => { const unsubscribeFunctions = dependencies.map(path => stateManager.subscribe(path, () => { const newValue = computeFnRef.current(); setComputedValue(newValue); }) ); // Initial computation setComputedValue(computeFn()); return () => { unsubscribeFunctions.forEach(unsubscribe => unsubscribe()); }; }, dependencies); return computedValue; } // Hook for state history tracking export function useStateHistory<T = any>( path: string, maxHistory: number = 50 ): [T, (value: T) => void, T[]] { const [value, setValue] = useStateValue<T>(path); const [history, setHistory] = useState<T[]>([]); const setValueWithHistory = useCallback((newValue: T) => { setValue(newValue); setHistory(prevHistory => { const newHistory = [...prevHistory, newValue].slice(-maxHistory); return newHistory; }); }, [setValue, maxHistory]); useEffect(() => { setHistory([value]); }, [path]); return [value, setValueWithHistory, history]; } // Hook for optimistic updates with rollback export function useOptimisticState<T = any>( path: string, optimisticUpdateFn: (currentValue: T) => Promise<T>, onError?: (error: Error) => void ): [T, (updateFn: (currentValue: T) => T) => Promise<void>, boolean] { const [value, setValue] = useStateValue<T>(path); const [isUpdating, setIsUpdating] = useState(false); const optimisticValueRef = useRef<T>(); const originalValueRef = useRef<T>(); const optimisticUpdate = useCallback(async (updateFn: (currentValue: T) => T) => { if (isUpdating) return; setIsUpdating(true); originalValueRef.current = value; try { // Apply optimistic update immediately const optimisticValue = updateFn(value); optimisticValueRef.current = optimisticValue; setValue(optimisticValue); // Apply the real update const result = await optimisticUpdateFn(value); // If the result differs from optimistic value, update it if (result !== optimisticValue) { setValue(result); } optimisticValueRef.current = undefined; originalValueRef.current = undefined; } catch (error) { // Rollback on error if (originalValueRef.current !== undefined) { setValue(originalValueRef.current); } optimisticValueRef.current = undefined; originalValueRef.current = undefined; logger.error('optimistic-state', 'Optimistic update failed', error as Error); onError?.(error as Error); } finally { setIsUpdating(false); } }, [value, isUpdating, optimisticUpdateFn, setValue, onError]); return [optimisticValueRef.current ?? value, optimisticUpdate, isUpdating]; } // Hook for state validation export function useStateValidation<T = any>( path: string, validator: (value: T) => string | null ): [T, (value: T) => void, string | null] { const [value, setValue] = useStateValue<T>(path); const [error, setError] = useState<string | null>(null); const setValidatedValue = useCallback((newValue: T) => { const validationError = validator(newValue); setError(validationError); if (!validationError) { setValue(newValue); } }, [setValue, validator]); useEffect(() => { const validationError = validator(value); setError(validationError); }, [value, validator]); return [value, setValidatedValue, error]; } // Hook for performance monitoring export function useStatePerformance(path: string): { value: any; renderCount: number; lastUpdate: Date; } { const [value, setValue] = useStateValue(path); const renderCountRef = useRef(0); const lastUpdateRef = useRef(new Date()); renderCountRef.current += 1; lastUpdateRef.current = new Date(); return { value, renderCount: renderCountRef.current, lastUpdate: lastUpdateRef.current }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DeamonDev888/Browser-Manager-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server