Skip to content
intermediatePhase 37 · Frontend Architecture

Server State vs Client State

Understand the difference and use appropriate tools for each.

45m
0 problems
Topic Progress0%

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

0/3solved
Build Server State vs Client State Component

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 needed
Server State vs Client State Testing

Write 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 tests
Server State vs Client State Performance

Optimize 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 analysis

Quiz

1. What is the key difference between server state and client state?

Question 1 options

2. What does staleTime control in React Query?

Question 2 options

3. What is the main advantage of SWR over manual fetch?

Question 3 options

4. How do you invalidate cache after a mutation in React Query?

Question 4 options

Flashcards

Question

What is server state?

Answer

Data that originates from the server, is asynchronous, shared across users, and can become stale.

Question

What does staleTime do in React Query?

Answer

Determines how long data is considered fresh before triggering a refetch.

Question

What is the stale-while-revalidate pattern?

Answer

Show cached (possibly stale) data immediately, then fetch fresh data in the background.

Question

When should you use SWR vs React Query?

Answer

SWR for simple fetching; React Query for complex apps with mutations, devtools, and infinite queries.

Question

What is Server State vs Client State?

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);