Documentation Index
Fetch the complete documentation index at: https://grantmaster.dev/llms.txt
Use this file to discover all available pages before exploring further.
State Management Strategy
This document outlines the patterns and tools used to manage data across the GrantMaster frontend. Our goal is a predictable, real-time, and high-performance UI.🏗️ The Hybrid Approach
We avoid a giant “Global Store” (like Redux) in favor of a hybrid strategy:1. Local State (useState, useReducer)
- Use for: Form inputs, toggle states, UI-only animations, and component-scoped data that doesn’t need to be shared.
- Rule: If the state isn’t needed by a sibling or parent, keep it local.
2. Context API (Feature-Scoped)
- Use for: Data shared across a significant branch of the component tree (e.g.,
TenantContext,UserAuthContext,GrantsListContext). - Rule: Use multiple small contexts rather than one large one to prevent unnecessary re-renders of the entire app.
3. Real-Time Synchronization (listenerManager)
- Use for: All Firestore data.
- Pattern: We do not “fetch” data once. We “subscribe” to it.
- The
listenerManagermanages FirestoreonSnapshotconnections. - It centralizes subscriptions to prevent memory leaks.
- When data changes in the DB, the UI updates automatically without a page refresh.
- The
🔄 Data Flow Pattern
- Subscription: A Hook (e.g.,
useGrants) calls thelistenerManagerto start a subscription for a specific collection or document. - Transformation: The Hook receives the raw Firestore “snapshots,” cleans them (e.g., converting Timestamps to JS Dates), and applies business logic.
- Delivery: The Hook provides the cleaned data and a
status(loading, error, success) to the component. - Action: User takes an action (e.g.,
createGrant). The component calls a Service Method (e.g.,GrantService.create). - Optimistic UI: The Service Method updates the database. Because we are subscribed to changes, the
listenerManagerreceives the update and the UI “pops” into place instantly.
🚀 Performance Optimizations
- Memoization: Use
useMemoanduseCallbackaround heavy transformation logic or callback functions passed to expensive child components. - Selectors: When consuming a large Context, ensure you are only re-rendering when the specific piece of data you need changes.
- Virtualization: For long lists (like the Grant Catalog), use windowing/virtualization to only render the items currently visible on the screen.
Shared Performance Utilities
Session Cache (src/lib/sessionCache.ts)
A lightweight in-memory cache for expensive computed results within a browser session. Useful for caching derived data (aggregations, transformed lists) that don’t change frequently but are expensive to recompute on every render.
Query Prefetcher (src/lib/queryPrefetcher.ts)
Prefetches React Query / tRPC data for anticipated navigations. Call prefetch() on hover or route preload to warm the cache before the user navigates, reducing perceived load time.
EventBus Query Bridge (src/lib/eventBusQueryBridge.ts)
Bridges the in-memory EventBus with React Query cache invalidation. When an EventBus event fires (e.g., expense.approved), the bridge automatically invalidates relevant React Query keys so stale server data is refetched without manual coordination.
Rich Text Editor (src/shared/editor/TiptapEditor.tsx)
A shared Tiptap-based rich text editor component for use across features that need formatted text input (report narratives, document editing, comment threads). Wraps Tiptap with the project’s design system styling.
🛠️ Developer Checklist
- Does this data need to persist after a page refresh? (If yes, it belongs in Firestore).
- Is this data used in more than 3 non-child components? (If yes, use a Context).
- Have I cleaned up the subscription in
useEffect? (ThelistenerManagershould handle this, but always verify). - Am I using a Service for the write operation rather than direct Firestore imports?