Data Loading
Fast first paint, smooth updates.
SSR vs SPA
Choose the right rendering strategy based on your requirements.
Server-Side Rendering
Use when:
- SEO is critical (crawlers need content)
- First contentful paint matters
- Data is mostly static or cacheable
- Users have slow devices
Single Page App
Use when:
- Behind authentication (no SEO needed)
- Highly interactive dashboards
- Real-time data updates
- Rich client-side interactions
SSR for important content
If you need SSR (for SEO or initial load performance), render important content on the server. Less critical data can be loaded client-side.
Trigger Fetching Early
Start data fetching as soon as possible — don't wait for components to render.
Use route loaders
React Router and TanStack Router support loaders that fetch data during navigation, before the component renders.
Don't block the render
Start the fetch early but defer non-critical data. Promises are handled automatically — show the page immediately with skeletons.
Batch database calls
Group related queries when possible to reduce round trips. But if a query is heavy, keep it separate to avoid blocking.
Stale-While-Revalidate
Show cached data immediately, fetch fresh data in the background.
Dashboard Stats
User count: 1,234
Show stale data first
Display cached data immediately for instant perceived performance. Users see content right away.
Show refetch indicator
When fetching fresh data, show a subtle spinner or indicator so users know an update is coming.
Smooth updates
When new data arrives, update the UI smoothly without jarring layout shifts.
Spin Delay
Prevent flickering skeletons for fast responses.
Data Loaded!
This content was fetched from the server.
Wait before showing skeleton
Only show loading UI if the request takes longer than ~200ms. Fast responses don't need skeletons.
Avoid UI flicker
A skeleton that appears and disappears in 50ms is jarring. Better to show nothing for quick loads.
Error Handling
Show errors inline, not as toasts. Make them recoverable.
User Profile
Alice Johnson
Inline error states
Show errors where the data was supposed to appear. Toasts get lost and overwhelm users with multiple failures.
Always offer retry
Include a retry button so users can recover without refreshing the entire page.
Report to observability
Log errors to your monitoring tools (Sentry, DataDog, etc.) so you can track and fix issues.
Realtime Data
Choose the right approach based on how "real" your realtime needs to be.
WebSocket
Use when:
- Updates need to be instant (<1 second)
- High frequency updates (chat, live scores)
- Bi-directional communication needed
- Many small messages over time
Interval HTTP
Use when:
- Updates every 30+ seconds is acceptable
- Simpler infrastructure needed
- Server doesn't support WebSocket
- Lower frequency dashboard updates
WebSocket for true realtime
HTTP requests have overhead (headers, connection setup). WebSocket maintains a persistent connection — much faster for frequent small updates.
Interval polling for 30s+ updates
If data only needs to refresh every 30 seconds or more, start with interval HTTP polling. Simpler to implement and debug.
Consider the trade-offs
WebSocket requires more infrastructure (connection management, reconnection logic). Don't over-engineer if polling works fine.
Cache Persistence
Not all data should be persisted. Some data must always be fresh.
Don't Persist
Data that must always be accurate:
- ✗Bank balance — Stale balance could cause overdrafts or confusion
- ✗Stock prices — Users make decisions based on current prices
- ✗Inventory count — Could lead to overselling
OK to Persist
Historical or reference data:
- ✓Transaction history — Past records don't change
- ✓User preferences — Settings rarely change
- ✓Static content — Blog posts, documentation
Think about the consequences
Ask: 'What happens if users see stale data?' If the answer is 'they might make bad decisions' — don't persist.
Show 'last fetched' timestamp
When showing persisted data, display when it was last fetched. Users need to know if they're seeing data from 5 minutes or 5 days ago.
Offline-first is an exception
If your app needs to work offline (like a notes app), persisting cache is required. But revalidate immediately when back online.
Set appropriate TTL
If you do persist, set a reasonable time-to-live. Stale cache from 3 months ago is probably useless.
Persistence Methods
LocalStorage
~5MB limitSimple key-value storage. Best for small, string-based data.
- Synchronous API — can block main thread
- Only stores strings (requires JSON.stringify)
- Good for: user preferences, tokens, small cache
IndexedDB
No practical limitFull database in the browser. Best for structured, large datasets.
- Asynchronous API — doesn't block UI
- Supports indexes, transactions, and cursors
- Good for: offline-first apps, large cache, complex queries
Embedded Database
ElectricSQL, Turso, PGlite, etc.Full SQL database running in the browser with sync capabilities.
- Real SQL queries (SQLite-based)
- Automatic sync with remote database
- Good for: offline-first apps, local-first architecture, complex data
Parsing Data from External Source
Always validate and parse data from APIs or databases to catch issues early.
Safe Parsing with Fallbacks
Without Parsing
What can go wrong:
- ✗Runtime crashes — Accessing
user.name.toUpperCase()when name is undefined - ✗Type mismatches — Expecting number, getting string like
"42" - ✗Silent failures — Missing fields that cause subtle bugs later
With Parsing
Benefits:
- ✓Early error detection — Catch invalid data at the boundary
- ✓Type safety — TypeScript knows the exact shape after parsing
- ✓Graceful handling — Use fallbacks or show meaningful errors
Parse at the boundary
Validate data as soon as it enters your application — right after fetch() or database query. Don't let invalid data propagate.
Use fallback values
For optional fields, provide sensible defaults. role: 'User' is better than role: undefined crashing your UI.
Show meaningful errors
When parsing fails, display a user-friendly message. Log the technical details to your observability tools.
Keep schemas in sync
When your API changes, update your validation schemas. Zod/Yup schemas are living documentation of your data contracts.