Day 13: TanStack Query for fetching contacts. Success: Cache and refetch data.

Lesson 13 60 min

Day 13: TanStack Query - The Invisible Hand Behind Hyperscale Data Fetching (Success: Cache and Refetch Data)

Welcome back, architects and engineers!

Yesterday, we wrestled with client-side UI state using Zustand, getting a feel for how a lightweight store can manage things like sidebar toggles. Today, we're stepping into a much grander arena: managing server state on the client. This might sound like an oxymoron, but it's where the magic happens for truly responsive, data-intensive applications like our AI-powered CRM.

We're going to dive into TanStack Query (formerly React Query). If you've ever built an app that fetches data, you know the dance: loading states, error handling, keeping data fresh, dealing with re-renders. TanStack Query isn't just another fetch wrapper; it's a sophisticated data synchronization layer that will fundamentally change how you think about client-server interaction.

Agenda for Day 13:

  1. Understanding the "Why": Why traditional data fetching falls short for complex UIs.

  2. Core Concepts: TanStack Query's role, caching, and the "stale-while-revalidate" pattern.

  3. Component Architecture: Integrating TanStack Query into our React frontend.

  4. Hands-on Build: Fetching and displaying a list of contacts.

  5. Insights for Hyperscale: How this seemingly frontend tool impacts system-wide performance and resilience.

The Problem with "Just Fetching"

Imagine our CRM. A sales rep opens a contact's profile. They navigate to another contact, then back to the first. Should the app refetch all the data for that first contact every single time? What if the network is flaky? What if the backend is momentarily slow?

Traditional data fetching often leads to:

  • "Loading Spinners Everywhere": A poor user experience, especially for frequently accessed data.

  • Manual Cache Management: Engineers writing brittle, error-prone code to store and retrieve data locally.

  • Stale Data: Users seeing outdated information because the app doesn't know when to refetch.

  • Race Conditions: Multiple components fetching the same data independently, leading to inconsistent UI states.

  • Increased Server Load: Unnecessary requests hammering your backend services.

This might be acceptable for a small internal tool, but for a hyperscale CRM handling millions of interactions daily, it's a recipe for disaster. Sales teams live and die by responsiveness. Every second wasted waiting for data is a potential lost deal.

Core Concepts: TanStack Query - Your Client-Side Backend Proxy

Think of TanStack Query as an intelligent, self-managing proxy living right inside your frontend. Its primary job is to keep your UI synchronized with your backend data, but it does so with an emphasis on performance, resilience, and developer experience.

The core system design concept here is Client-Side Data Caching and Synchronization.

Architecture and Control Flow:

Component Architecture

React Frontend QueryClientProvider ContactList (useQuery) Query Cache Logic API
  1. Provider: You wrap your application with a QueryClientProvider. This sets up the global cache and configuration.

  2. useQuery Hook: In your component, you use useQuery with a unique query key (an array) and a query function (your async data fetching logic).

  3. Cache Check: When useQuery is called, TanStack Query first checks its internal cache using the query key.

  • Cache Hit (Fresh): If the data is in the cache and considered "fresh" (based on staleTime), it's returned immediately. No network request.

  • Cache Hit (Stale): If data is in the cache but "stale" (past staleTime), it's returned immediately, and a background refetch is initiated. This is the stale-while-revalidate pattern – the user sees data instantly, and it's updated silently in the background. This is a massive win for perceived performance.

  • Cache Miss: No data in cache. A network request is initiated.

  1. Network Request: Your queryFn executes, fetching data from your API.

  2. Cache Update: Once data arrives, it's stored in the cache and marked as fresh.

  3. UI Update: Your component re-renders with the latest data, along with isLoading, isError, isSuccess states.

Data Flow and State Changes:

Flowchart diagram

Mount In Cache? Hard Loading Is Stale? Show Cached + Background Fetch

The data flows from your backend API, through TanStack Query's cache, and into your components. The state of a query isn't just "loading" or "done"; it's a nuanced lifecycle:

  • idle: The query is not yet active.

  • loading: Data is being fetched for the first time.

  • success: Data has been successfully fetched and is available.

  • error: An error occurred during fetching.

  • stale: Data is in the cache but considered outdated and might trigger a background refetch.

  • fetching: A background refetch is currently in progress (even if success data is already displayed).

This granular state management allows you to build incredibly robust and user-friendly interfaces.

Size and Real-Time Production System Application

For an ultra-high-scale CRM handling 100 million requests per second (RPS) at peak, every optimization counts. While TanStack Query operates on the client, its impact ripples through the entire system:

  1. Reduced Backend Load: Intelligent caching drastically cuts down redundant GET requests to your contact, deal, and activity microservices. This frees up database connections, CPU cycles, and network bandwidth on your backend, allowing it to serve more unique requests and scale more efficiently.

  2. Enhanced User Experience (UX): "Stale-while-revalidate" means sales reps get instant feedback. This isn't just a nicety; it's a productivity multiplier. In a high-pressure sales environment, waiting even a second can break a rep's flow.

  3. Network Resilience: If a user is on a spotty mobile connection, TanStack Query can serve cached data, making the app feel robust even when the network isn't. Background refetching means data eventually becomes consistent without explicit user action.

  4. Simplified Development: Engineers spend less time on boilerplate data fetching logic and more time on core CRM features, leading to faster iteration and higher quality code.

This isn't about just fetching contacts; it's about building a CRM that feels instant, reliable, and intelligent, even under immense load.

Hands-on Build-Along: Fetching Contacts

Let's get our hands dirty. We'll set up a simple backend to serve contact data and then integrate TanStack Query into our React frontend to fetch and display it.

Project Structure (as created by start.sh):

Code
crm-app/
├── backend/
│ └── server.js
├── frontend/
│ ├── src/
│ │ ├── App.tsx
│ │ ├── main.tsx
│ │ └── components/
│ │ └── ContactList.tsx
│ └── package.json
└── package.json

1. Backend (backend/server.js)

This will be a super simple Express server to mock contact data.

javascript
// backend/server.js
const express = require('express');
const cors = require('cors'); // Import cors middleware
const app = express();
const port = 3001;

// Use cors middleware to allow cross-origin requests from our frontend
app.use(cors());
app.use(express.json());

let contacts = [
{ id: 'c1', name: 'Alice Johnson', email: 'alice@example.com', phone: '555-1001' },
{ id: 'c2', name: 'Bob Williams', email: 'bob@example.com', phone: '555-1002' },
{ id: 'c3', name: 'Charlie Brown', email: 'charlie@example.com', phone: '555-1003' },
{ id: 'c4', name: 'Diana Prince', email: 'diana@example.com', phone: '555-1004' },
{ id: 'c5', name: 'Eve Adams', email: 'eve@example.com', phone: '555-1005' },
];

app.get('/api/contacts', (req, res) => {
console.log('Fetching contacts...');
// Simulate network delay
setTimeout(() => {
res.json(contacts);
}, 500); // 500ms delay
});

app.listen(port, () => {
console.log(`Backend server running at http://localhost:${port}`);
});

2. Frontend Setup (frontend/src/main.tsx)

We need to set up QueryClientProvider at the root of our React app.

typescript
// frontend/src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // Optional: for debugging

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // Data is considered fresh for 5 minutes
cacheTime: 1000 * 60 * 10, // Data stays in cache for 10 minutes(even if unused)
refetchOnWindowFocus: true, // Refetch when window regains focus
},
},
});

ReactDOM.createRoot(document.getElementById('root')!).render(

{/* Devtools for debugging */}

,
);

3. Contact List Component (frontend/src/components/ContactList.tsx)

This component will use useQuery to fetch and display our contacts.

typescript
// frontend/src/components/ContactList.tsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';

interface Contact {
id: string;
name: string;
email: string;
phone: string;
}

const fetchContacts = async (): Promise<Contact[]> => {
const response = await fetch('http://localhost:3001/api/contacts');
if (!response.ok) {
throw new Error('Failed to fetch contacts');
}
return response.json();
};

const ContactList: React.FC = () => {
const { data, isLoading, isError, error, isFetching } = useQuery<Contact[], Error>({
queryKey: ['contacts'], // Unique key for this query
queryFn: fetchContacts,
});

if (isLoading) {
return

Loading contacts...;
}

if (isError) {
return

Error: {error?.message};
}

return (

## Customer Contacts {isFetching ? (updating...) : ''}

- 
{data?.map((contact) => ({contact.name}

{contact.email}

{contact.phone}

))}

);
};

export default ContactList;

4. App Component (frontend/src/App.tsx)

Just rendering our ContactList.

typescript
// frontend/src/App.tsx
import ContactList from './components/ContactList';
import './App.css'; // Assuming some basic CSS for styling

function App() {
return (

 );
}

export default App;

5. Basic CSS (frontend/src/index.css and frontend/src/App.css)

Let's add some basic Tailwind CSS classes for a professional look.

css
/* frontend/src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Add any global styles here if needed */
css
/* frontend/src/App.css */
/* Minimal custom CSS, mostly relying on Tailwind */

Verification:

State Machine diagram

IDLE FETCHING SUCCESS ERROR
  1. Run start.sh.

  2. Open your browser to http://localhost:5173.

  3. You should see "Loading contacts..." briefly, then the list of contacts.

  4. Test Caching: Navigate to another tab, wait a few seconds, then come back to the CRM tab. You should see the data appear instantly because it's served from the cache. TanStack Query might trigger a background refetch if staleTime has passed, but the UI won't show a loading spinner, only a subtle "updating..." if isFetching is true.

  5. Test Refetching: Keep the console open. You'll notice Fetching contacts... logs appearing in the backend terminal periodically if refetchOnWindowFocus is true and you switch tabs.

This hands-on exercise demonstrates the power of TanStack Query in providing a snappy, responsive experience, which is critical for any production-grade system, let alone a hyperscale CRM.

Assignment: Level Up Your CRM Data Fetching

Your task is to extend our contact fetching capabilities.

  1. Add a Search/Filter Input:

  • Implement an input field in the ContactList component.

  • When the user types, modify the queryKey for useQuery to include the search term (e.g., ['contacts', searchTerm]). This will tell TanStack Query that you're requesting different data, triggering a new fetch.

  • Modify your backend /api/contacts endpoint to accept a searchTerm query parameter (e.g., GET /api/contacts?search=Alice) and filter the contacts array before sending.

  • Insight: Notice how changing the queryKey automatically handles caching for different search results!

  1. Display "Last Updated" Time:

  • Show the time when the data was last successfully fetched. TanStack Query provides dataUpdatedAt or isStale properties that can help here.

Solution Hints:

  1. Search Input:

  • In ContactList.tsx, use useState to manage the searchTerm.

  • Update queryKey to ['contacts', searchTerm].

  • Modify fetchContacts to accept a searchTerm parameter and append it to the URL: http://localhost:3001/api/contacts?search=${searchTerm}.

  • In backend/server.js, access req.query.search and filter the contacts array.

  1. Last Updated:

  • useQuery returns dataUpdatedAt (timestamp of last successful data update) and isStale. You can format dataUpdatedAt into a human-readable string.

Need help?