Understanding Server State
Understanding Server State
Server state is data that originates from and belongs to the server. Unlike client state, it's asynchronous, shared, and can become stale.
Key Characteristics
- Asynchronous: Requires network requests to fetch/update
- Shared: Multiple users see the same data
- Stale: Can change independently of client actions
- Circular: Server state often contains references to other server entities
// Client state - belongs to the component
const [isOpen, setIsOpen] = useState(false);
// Server state - belongs to the server
const { data: products, isLoading } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
});
Problems with Treating Server State as Client State
// ❌ Bad: Manual state management for server data
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
// Manual cache invalidation
const deleteProduct = async (id) => {
await fetch(`/api/products/${id}`, { method: 'DELETE' });
setProducts(products.filter(p => p.id !== id));
};
// Loading, error, stale data handling all manual
}
// ✅ Good: React Query handles it
function ProductList() {
const queryClient = useQueryClient();
const { data: products, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
});
const deleteMutation = useMutation({
mutationFn: (id) => fetch(`/api/products/${id}`, { method: 'DELETE' }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
}
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
<button onClick={() => deleteMutation.mutate(product.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
Benefits of Proper Server State Management
- Automatic caching and revalidation
- Background refetching
- Optimistic updates
- Deduplication of requests
- Stale-while-revalidate patterns
Caching Server State
Caching Server State
Proper caching reduces network requests and improves user experience.
Cache Invalidation Strategies
// Time-based (stale after 5 minutes)
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes (garbage collection)
});
// Event-based (invalidate on mutation)
const queryClient = useQueryClient();
const addProduct = useMutation({
mutationFn: createProduct,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
}
});
// Dependent queries
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
const { data: orders } = useQuery({
queryKey: ['orders', user?.id],
queryFn: () => fetchOrders(user.id),
enabled: !!user?.id, // Only run when user is available
});
Cache Patterns
// Optimistic updates
const updateTodo = useMutation({
mutationFn: (todo) => fetch(`/api/todos/${todo.id}`, {
method: 'PUT',
body: JSON.stringify(todo)
}),
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) =>
old.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
Prefetching
function ProductList() {
const queryClient = useQueryClient();
return (
<ul>
{products.map(product => (
<li
key={product.id}
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ['product', product.id],
queryFn: () => fetchProduct(product.id),
staleTime: 60000
});
}}
>
{product.name}
</li>
))}
</ul>
);
}
SWR and React Query
SWR and React Query
SWR (Stale-While-Revalidate)
Lightweight data fetching library by Vercel:
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
const fetcher = (url) => fetch(url).then(r => r.json());
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (isLoading) return <Spinner />;
if (error) return <div>Error loading profile</div>;
return <div>Hello, {data.name}!</div>;
}
// With mutations
async function updateUser(url, { arg }) {
return fetch(url, {
method: 'PUT',
body: JSON.stringify(arg)
}).then(r => r.json());
}
function EditProfile() {
const { trigger, isMutating } = useSWRMutation('/api/user', updateUser);
const handleSubmit = async (formData) => {
await trigger({ name: formData.name });
};
return (
<form onSubmit={handleSubmit}>
<input name="name" />
<button disabled={isMutating}>Save</button>
</form>
);
}
React Query (TanStack Query)
More feature-rich data fetching library:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function TodoApp() {
const queryClient = useQueryClient();
// Query
const { data: todos, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
staleTime: 1000 * 60 * 5
});
// Mutation
const addTodo = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
}).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
return (
<div>
<button onClick={() => addTodo.mutate({ text: 'New Todo' })}>
Add Todo
</button>
{isLoading ? (
<Spinner />
) : (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)}
</div>
);
}
Choosing Between SWR and React Query
| Feature | SWR | React Query |
|---|---|---|
| Bundle Size | ~4kB | ~13kB |
| Features | Basic | Full-featured |
| Mutations | Basic | Advanced |
| DevTools | No | Yes |
| Infinite Queries | No | Yes |
Use SWR for simple data fetching with minimal setup.
Use React Query for complex apps with mutations, optimistic updates, and dev tools.
Practice Problems
Create a reusable React component implementing Server State vs Client State. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Server State vs Client State using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Server State vs Client State for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is the key difference between server state and client state?
2. What does staleTime control in React Query?
3. What is the main advantage of SWR over manual fetch?
4. How do you invalidate cache after a mutation in React Query?
Flashcards
Question
What is server state?
Click to reveal answer
Answer
Data that originates from the server, is asynchronous, shared across users, and can become stale.
Question
What does staleTime do in React Query?
Click to reveal answer
Answer
Determines how long data is considered fresh before triggering a refetch.
Question
What is the stale-while-revalidate pattern?
Click to reveal answer
Answer
Show cached (possibly stale) data immediately, then fetch fresh data in the background.
Question
When should you use SWR vs React Query?
Click to reveal answer
Answer
SWR for simple fetching; React Query for complex apps with mutations, devtools, and infinite queries.
Question
What is Server State vs Client State?
Click to reveal answer
Answer
Server State vs Client State is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.Server state is fundamentally different from client state - it's async and can become stale
- 2.React Query and SWR handle caching, revalidation, and background updates automatically
- 3.Use staleTime to control when data needs refetching
- 4.Invalidate cache after mutations to keep UI in sync
- 5.Choose SWR for simplicity, React Query for advanced features
Interview Tips
- •Explain why server state needs special handling compared to client state
- •Describe the stale-while-revalidate pattern and its benefits
- •Know when to invalidate cache and how to handle dependent queries
Cheat Sheet
Server State Cheat Sheet
Key Concepts
- Server state is async, shared, and stale
- Client state is sync, local, and fresh
React Query Setup
const { data, isLoading, error } = useQuery({
queryKey: ['key'],
queryFn: fetchData
});
Cache Invalidation
queryClient.invalidateQueries({ queryKey: ['products'] });
SWR Basic
const { data } = useSWR('/api/data', fetcher);