UNPKG

ws-dottie

Version:

Your friendly TypeScript companion for Washington State transportation APIs - WSDOT and WSF data with smart caching and React Query integration

618 lines (507 loc) 17.8 kB
# TanStack Query Integration Guide This guide covers integrating WS-Dottie with TanStack Query for optimal data fetching, caching, and state management in React applications. > **📚 Documentation Navigation**: [Documentation Index](../../INDEX.md) • [Getting Started](../../getting-started.md) • [API Guide](../api-guide.md) ## 🚀 Quick Start ### 1. Install Dependencies ```bash # Install WS-Dottie and TanStack Query npm install ws-dottie @tanstack/react-query # Or with yarn yarn add ws-dottie @tanstack/react-query ``` ### 2. Set Up TanStack Query ```jsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // Create a client with optimized defaults for WS-Dottie const queryClient = new QueryClient({ defaultOptions: { queries: { // Global cache configuration staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 10 * 60 * 1000, // 10 minutes refetchOnWindowFocus: false, retry: (failureCount, error) => { // Retry on network errors, not on 4xx client errors if (error.status >= 500) return failureCount < 3; return false; }, }, }, }); // Wrap your app with QueryClientProvider function App() { return ( <QueryClientProvider client={queryClient}> <YourTransportationApp /> </QueryClientProvider> ); } ``` ### 3. Use WS-Dottie Hooks ```jsx import { useVesselLocations } from 'ws-dottie/wsf-vessels'; import { useAlerts } from 'ws-dottie/wsdot-highway-alerts'; function TransportationDashboard() { const { data: vessels, isLoading, error } = useVesselLocations({ fetchMode: 'native', validate: false, }); const { data: alerts } = useAlerts({ fetchMode: 'native', validate: true, }); if (error) return <div>Error: {error.message}</div>; return ( <div> <h1>Washington Transportation</h1> {isLoading && <div>Loading...</div>} <section> <h2>Ferries ({vessels?.length || 0})</h2> {vessels?.map(vessel => ( <div key={vessel.VesselID}> <h3>{vessel.VesselName}</h3> <p>Location: {vessel.Latitude}, {vessel.Longitude}</p> <p>Speed: {vessel.Speed} knots</p> </div> ))} </section> <section> <h2>Highway Alerts ({alerts?.length || 0})</h2> {alerts?.map(alert => ( <div key={alert.AlertID}> <h3>{alert.HeadlineDescription}</h3> <p>Priority: {alert.Priority}</p> </div> ))} </section> </div> ); } ``` ### Hook Parameters Align With Fetch Functions WS-Dottie hooks accept the same `FetchFunctionParams<T>` object as their matching fetch functions. Provide endpoint parameters with the `params` property, override transport with `fetchMode`, and toggle Zod validation with `validate`. TanStack Query options remain available as an optional second argument to every hook. ## 🔄 Cache Strategies WS-Dottie provides four cache strategies optimized for different types of transportation data: | Strategy | Stale Time | Refetch Interval | Use Cases | |-----------|--------------|------------------|------------| | `REALTIME` | 5s | 5s | Vessel locations, traffic flow | | `FREQUENT` | 5m | 5m | Highway alerts, weather information | | `MODERATE` | 1h | 1h | Mountain pass conditions, travel times | | `STATIC` | 1d | 1d | Terminal locations, schedules, fares | ### Using Cache Strategies ```jsx import { useVesselLocations } from 'ws-dottie/wsf-vessels'; import { useAlerts } from 'ws-dottie/wsdot-highway-alerts'; import { useTerminalLocations } from 'ws-dottie/wsf-terminals'; // Use default caching strategy (REALTIME for vessel locations) const { data: vessels } = useVesselLocations({ fetchMode: 'native', validate: false, }); // Override with custom caching strategy const { data: alerts } = useAlerts( { fetchMode: 'native', validate: true }, { staleTime: 2 * 60 * 1000, // 2 minutes instead of 5 refetchInterval: 2 * 60 * 1000, // 2 minutes instead of 5 } ); // Use with static data const { data: terminals } = useTerminalLocations({ fetchMode: 'native', validate: true, }); // Uses STATIC strategy ``` ## 🔄 Cache Invalidation WSF APIs provide cache flush dates to help determine when data has been updated. WS-Dottie includes automatic cache invalidation for WSF APIs with STATIC cache strategy. ### Understanding Cache Flush Endpoints WSF APIs include special `cacheFlushDate` endpoints that return timestamps indicating when underlying data was last updated. These endpoints serve a critical purpose: - **Data Freshness**: They tell you exactly when source data was last modified - **Efficient Updates**: Instead of polling frequently, you can check if data has changed - **Bandwidth Savings**: Prevents unnecessary data transfers when nothing has changed - **User Experience**: Ensures users see fresh data without long loading times ### Automatic Cache Invalidation The factory system automatically detects WSF APIs with STATIC cache strategy and integrates with cache flush date hooks from `src/shared/cache/cacheFlushDate.ts`: - **Automatic Detection**: Hooks created for WSF APIs (wsf-fares, wsf-schedule, wsf-terminals, wsf-vessels) with STATIC cache strategy automatically use cache flush date invalidation - **Polling**: The `useCacheFlushDate` hook polls the relevant cache flush date endpoint every 5 minutes - **Change Detection**: The `useInvalidateOnFlushChange` hook compares the current flush date with the previous value stored in a React ref - **Cache Invalidation**: When the flush date changes, TanStack Query's `invalidateQueries` is called to refresh the cached data - **No Persistence**: Flush dates are tracked in-memory using React refs (not localStorage) for the duration of the component lifecycle The cache flush date system works by: 1. Each hook created for a WSF API with STATIC strategy automatically calls `useCacheFlushDate(api.name)` internally 2. The flush date query runs independently with a 5-minute refetch interval 3. `useInvalidateOnFlushChange` monitors the flush date and invalidates the endpoint's cache when it changes 4. This ensures data is refreshed only when the source data actually changes, not on a fixed schedule ### Using Hooks with Automatic Cache Invalidation ```jsx import { useTerminalLocations } from 'ws-dottie/wsf-terminals'; function FerryTerminals() { // Terminal locations hook includes automatic cache invalidation // Parameters are optional for endpoints that don't require them const { data: terminals, isLoading, error } = useTerminalLocations({ fetchMode: 'native', validate: true, }); if (isLoading) return <div>Loading terminals...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h1>Washington State Ferry Terminals</h1> {terminals?.map(terminal => ( <div key={terminal.TerminalID}> <h3>{terminal.TerminalName}</h3> <p>{terminal.Address}</p> </div> ))} </div> ); } ``` ### Using with Parameters ```jsx import { useVesselBasics } from 'ws-dottie/wsf-vessels'; function VesselDetails({ vesselId }) { // Vessel basics hook includes automatic cache invalidation // Parameters are required for endpoints that need them const { data: vessel, isLoading, error } = useVesselBasics({ params: { VesselID: vesselId }, fetchMode: 'native', validate: true, }); if (isLoading) return <div>Loading vessel details...</div>; if (error) return <div>Error: {error.message}</div>; return ( <div> <h1>{vessel?.VesselName}</h1> <p>Class: {vessel?.VesselClass}</p> <p>Capacity: {vessel?.VehicleCapacity} vehicles</p> </div> ); } ``` ## 🎛️ Advanced Query Options WS-Dottie hooks support all TanStack Query options for fine-grained control: ### Custom Query Options ```jsx import { useVesselLocations } from 'ws-dottie/wsf-vessels'; function VesselTracker() { const { data: vessels, isLoading, error, refetch } = useVesselLocations( { fetchMode: 'native', validate: false }, { // Override default cache strategy staleTime: 10 * 1000, // 10 seconds instead of 5 refetchInterval: 10 * 1000, // 10 seconds instead of 5 // Custom retry logic retry: (failureCount, error) => { if (error.status === 404) return false; // Don't retry 404s if (failureCount < 5) return true; // Retry up to 5 times return false; }, // Custom error handling onError: (error) => { console.error('Vessel fetch error:', error); // Show user-friendly message if (error.status === 401) { showNotification('Please check your API key configuration'); } else if (error.status >= 500) { showNotification('Server error. Please try again later.'); } }, // Success callback onSuccess: (data) => { console.log(`Loaded ${data.length} vessels`); }, } ); if (error) { return ( <div> <h3>Error Loading Data</h3> <p>{error.message}</p> <button onClick={() => refetch()}>Retry</button> </div> ); } if (isLoading) return <div>Loading vessels...</div>; return ( <div> {vessels?.map(vessel => ( <div key={vessel.VesselID}> <h3>{vessel.VesselName}</h3> <p>Speed: {vessel.Speed} knots</p> </div> ))} </div> ); } ``` ### Conditional Data Fetching ```jsx import { useVesselLocations } from 'ws-dottie/wsf-vessels'; import { useTerminalWaitTimes } from 'ws-dottie/wsf-terminals'; function FerryDashboard() { const [showDetails, setShowDetails] = useState(false); // Always fetch vessel locations const { data: vessels } = useVesselLocations({ fetchMode: 'native', validate: false, }); // Conditionally fetch terminal wait times const { data: waitTimes } = useTerminalWaitTimes( { fetchMode: 'native', validate: true }, { enabled: showDetails // Only fetch when details are shown } ); return ( <div> <h1>Ferry Dashboard</h1> <button onClick={() => setShowDetails(!showDetails)}> {showDetails ? 'Hide Details' : 'Show Details'} </button> <div className="vessel-list"> {vessels?.map(vessel => ( <div key={vessel.VesselID}> <h3>{vessel.VesselName}</h3> <p>Location: {vessel.Latitude}, {vessel.Longitude}</p> {showDetails && waitTimes && ( <div className="vessel-details"> <p>Terminal Wait: { waitTimes.find(t => t.TerminalID === vessel.DestinationTerminalID)?.WaitTime || 'Unknown' } minutes</p> </div> )} </div> ))} </div> </div> ); } ``` ## 🎯 React Patterns ### Data Fetching with Parameters ```jsx import { useScheduleByTripDateAndRouteId } from 'ws-dottie/wsf-schedule'; import { useFareLineItemsByTripDateAndTerminals } from 'ws-dottie/wsf-fares'; function FerrySchedule() { const [route, setRoute] = useState({ departing: 3, arriving: 7, date: new Date() }); const { data: schedules } = useScheduleByTripDateAndRouteId({ params: { TripDate: route.date.toISOString().split('T')[0], RouteID: route.departing }, fetchMode: 'native', validate: true, }); const { data: fares } = useFareLineItemsByTripDateAndTerminals({ params: { TripDate: route.date.toISOString().split('T')[0], DepartingTerminalID: route.departing, ArrivingTerminalID: route.arriving }, fetchMode: 'native', validate: true, }); return ( <div> <div className="route-selector"> <label>From: <select value={route.departing} onChange={e => setRoute({...route, departing: Number(e.target.value)})} > {/* Terminal options */} </select> </label> <label>To: <select value={route.arriving} onChange={e => setRoute({...route, arriving: Number(e.target.value)})} > {/* Terminal options */} </select> </label> <label>Date: <input type="date" value={route.date.toISOString().split('T')[0]} onChange={e => setRoute({...route, date: new Date(e.target.value)})} /> </label> </div> <div className="schedule-display"> {/* Render schedules and fares */} </div> </div> ); } ``` ### Component Structure Recommended component structure for WS-Dottie data: ```jsx // Container component - handles data fetching function VesselMapContainer() { const { data: vessels, isLoading, error, refetch } = useVesselLocations({ fetchMode: 'native', validate: false, }); return ( <VesselMap vessels={vessels || []} isLoading={isLoading} error={error} onRefresh={refetch} /> ); } // Presentational component - handles UI rendering function VesselMap({ vessels, isLoading, error, onRefresh }) { if (error) return <ErrorDisplay error={error} />; if (isLoading) return <LoadingDisplay />; return ( <div> <button onClick={onRefresh}>Refresh</button> {/* Map rendering logic */} </div> ); } ``` ### Data Transformation ```jsx import { useWeatherInformation } from 'ws-dottie/wsdot-weather-information'; function WeatherDashboard() { const { data: weather } = useWeatherInformation({ fetchMode: 'native', validate: true, }); // Transform data for visualization const weatherByRegion = useMemo(() => { if (!weather) return {}; return weather.reduce((acc, station) => { const region = station.Region || 'Unknown'; if (!acc[region]) acc[region] = []; acc[region].push(station); return acc; }, {}); }, [weather]); const regionStats = useMemo(() => { return Object.entries(weatherByRegion).map(([region, stations]) => ({ region, count: stations.length, avgTemp: stations.reduce((sum, s) => sum + (s.Temperature || 0), 0) / stations.length, maxTemp: Math.max(...stations.map(s => s.Temperature || 0)), minTemp: Math.min(...stations.map(s => s.Temperature || 0)), })); }, [weatherByRegion]); return ( <div> <h1>Weather Dashboard</h1> <div className="region-stats"> {regionStats.map(stat => ( <div key={stat.region}> <h3>{stat.region}</h3> <p>Stations: {stat.count}</p> <p>Avg Temp: {stat.avgTemp.toFixed(1)}°F</p> <p>Range: {stat.minTemp}°F - {stat.maxTemp}°F</p> </div> ))} </div> </div> ); } ``` ## 🎯 Best Practices ### 1. Choose Appropriate Caching Strategy ```jsx import { useVesselLocations } from 'ws-dottie/wsf-vessels'; import { useTerminalWaitTimes, useTerminalLocations } from 'ws-dottie/wsf-terminals'; import { useScheduleByTripDateAndRouteId } from 'ws-dottie/wsf-schedule'; function FerryApp() { // Real-time data for live tracking const { data: vessels } = useVesselLocations({ fetchMode: 'native', validate: false, }); // Uses REALTIME strategy // Frequent updates for wait times const { data: waitTimes } = useTerminalWaitTimes({ fetchMode: 'native', validate: true, }); // Uses FREQUENT strategy // Static data with automatic cache invalidation const { data: terminals } = useTerminalLocations({ fetchMode: 'native', validate: true, }); // Uses cache invalidation instead of fixed interval return ( <div> {/* Component implementation */} </div> ); } ``` ### 2. Batch Requests for Performance ```jsx import { useQueries } from '@tanstack/react-query'; import { fetchVesselLocations } from 'ws-dottie/wsf-vessels/core'; import { fetchTerminalWaitTimes } from 'ws-dottie/wsf-terminals/core'; function FerryDashboard() { // Fetch multiple data sources in parallel with useQueries const results = useQueries({ queries: [ { queryKey: ['vessels'], queryFn: () => fetchVesselLocations({ fetchMode: 'native', validate: true }), staleTime: 5 * 1000, // 5 seconds }, { queryKey: ['waitTimes'], queryFn: () => fetchTerminalWaitTimes({ fetchMode: 'native', validate: true }), staleTime: 60 * 1000, // 1 minute }, ], }); const [vessels, waitTimes] = results.map(result => result.data); return ( <div> {/* Component implementation */} </div> ); } ``` ### 3. Manual Cache Invalidation ```jsx import { useScheduleByTripDateAndRouteId } from 'ws-dottie/wsf-schedule'; import { useQueryClient } from '@tanstack/react-query'; function ScheduleComponent() { const queryClient = useQueryClient(); const { data: schedules } = useScheduleByTripDateAndRouteId({ fetchMode: 'native', validate: true, }); const refreshSchedules = () => { queryClient.invalidateQueries(['schedules']); }; return ( <div> <button onClick={refreshSchedules}> Refresh Schedules </button> {/* Display schedules */} </div> ); } ``` ## 📚 Next Steps - **[Fetching Data Guide](./fetching-data.md)** - Basic fetch-dottie usage patterns - **[CLI Usage Guide](./cli-usage.md)** - Command-line interface and debugging - **[Error Handling Reference](./error-handling.md)** - WS-Dottie error types and recovery