ReactData FetchingCachingUXState Management

React Query: Server State Solved 🛡️

T
Tanner Linsley
Featured Guide 30 min read

"Using useEffect for data fetching is the most common self-inflicted wound in React development. Stop it."

Fetching data is easy. Handling loading states, error states, caching, deduping, revalidation, focus-refetching, and pagination is hard.

React Query removes the need for useEffect entirely.
Server State (remote, shared, async) is different from Client State (local, synchronous). Treat them differently.

02. Stale Time vs Cache Time

This confuses 99% of developers. Here is the definitive explanation.

⏱️ Stale Time

"How long until I consider this data 'old' and should refetch?"
Default: 0 (Immediately refetch on mount).

🗑️ Cache Time

"How long until I delete the data from memory entirely?"
Default: 5 minutes.

If staleTime is 5 minutes, React Query won't refetch even if you mount the component 100 times. It serves from cache instantly.

03. Optimistic Updates (The Magic)

Update the UI immediately when the user clicks. Don't wait for the server. If it fails, rollback. This makes your app feel instant, like a native iOS app.

const mutation = useMutation({
  mutationFn: newTodo => axios.post('/todos', newTodo),
  // When mutate is called:
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries(['todos'])
    const previousTodos = queryClient.getQueryData(['todos'])
    // Optimistically update to the new value
    queryClient.setQueryData(['todos'], old => [...old, newTodo])
    return { previousTodos }
  },
  onError: (err, newTodo, context) => {
    // Rollback ONLY if error
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
})

04. Infinite Queries

Pagination is boring. Infinite scroll is where it's at. useInfiniteQuery handles the complexity of `nextPageParam`, merging pages, and bidirectional fetching for you.

05. Share the Knowledge

"If you are putting API data into Redux/Context, you are doing it wrong. That's Server State. Use React Query and delete 50% of your code. #ReactJS #WebDev"

Twitter / X

The Takeaway

Your app feels faster not because the server is faster, but because you are lying to the user (Optimistically). And that's okay.

06. Fetch Visualizer

Simulate network latency and see how optimistic updates trick the user into thinking the app is instant.

Concept Optimistic UI Mode

Toggle "Optimistic Mode" inside the demo. When ON, the list updates instantly. When OFF, it waits for the "Server" (1.5s delay).

Interactive Playground

import React, { useState } from 'react';

// ----------------------------------------------------
// 🔴 REACT QUERY SIMULATOR
// ----------------------------------------------------

export default function QuerySimulator() {
  const [todos, setTodos] = useState([{ id: 1, text: 'Master React Query' }]);
  const [optimistic, setOptimistic] = useState(false);
  const [input, setInput] = useState('');
  const [isPending, setIsPending] = useState(false);

  const handleAdd = () => {
      if(!input) return;
      const newTodo = { id: Date.now(), text: input };
      setInput('');

      if(optimistic) {
          // 1. Optimistic Update (Immediate)
          setTodos(prev => [...prev, { ...newTodo, optimistic: true }]);
          
          // 2. Simulate Network Request
          setIsPending(true);
          setTimeout(() => {
              // 3. Server Confirms (Remove optimistic flag)
               setTodos(prev => prev.map(t => t.id === newTodo.id ? { ...t, optimistic: false } : t));
               setIsPending(false);
          }, 1500);
      } else {
          // 1. Standard Way (Wait for server)
          setIsPending(true);
          setTimeout(() => {
              setTodos(prev => [...prev, newTodo]);
              setIsPending(false);
          }, 1500);
      }
  };

  return (
    <div className="bg-white dark:bg-[#111] text-gray-900 dark:text-gray-200 border border-gray-200 dark:border-gray-800 rounded-2xl overflow-hidden shadow-2xl p-8 flex flex-col md:flex-row gap-8 h-[500px]">
      
      {/* Controls */}
      <div className="w-full md:w-1/3 space-y-6">
          <div className="space-y-4">
               <h3 className="text-xl font-bold">1. Config</h3>
               <div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-900 rounded-xl">
                   <span className="font-bold text-sm">Optimistic Mode</span>
                   <button 
                     onClick={() => setOptimistic(!optimistic)}
                     className={`w-12 h-6 rounded-full p-1 transition-colors ${optimistic ? 'bg-red-500' : 'bg-gray-300 dark:bg-gray-600'}`}
                   >
                       <div className={`w-4 h-4 rounded-full bg-white shadow-sm transition-transform ${optimistic ? 'translate-x-6' : 'translate-x-0'}`}></div>
                   </button>
               </div>
          </div>

          <div className="space-y-4">
               <h3 className="text-xl font-bold">2. Mutation</h3>
               <div className="flex gap-2">
                   <input 
                     value={input}
                     onChange={(e) => setInput(e.target.value)}
                     className="flex-1 bg-white dark:bg-black border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2 focus:ring-2 ring-red-500 outline-none"
                     placeholder="New Todo..."
                   />
                   <button 
                     onClick={handleAdd}
                     className="bg-red-500 text-white px-4 rounded-lg font-bold hover:bg-red-600 transition-colors"
                   >
                       Add
                   </button>
               </div>
          </div>
      </div>

      {/* Visualization */}
      <div className="flex-1 bg-gray-50 dark:bg-black/50 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 relative overflow-hidden">
           <div className="text-xs text-gray-400 font-bold uppercase mb-4 tracking-widest">Server State</div>
           
           <div className="space-y-2">
               {todos.map(todo => (
                   <div 
                     key={todo.id} 
                     className={`p-4 rounded-xl flex justify-between items-center transition-all bg-white dark:bg-[#1a1a1a] border border-gray-100 dark:border-gray-800 ${todo.optimistic ? 'opacity-50 ring-2 ring-red-500/50 grayscale' : ''}`}
                   >
                       <span className="font-medium">{todo.text}</span>
                       {todo.optimistic && <span className="text-[10px] font-bold text-red-500 bg-red-100 dark:bg-red-900/40 px-2 py-0.5 rounded">SAVING...</span>}
                   </div>
               ))}
               
               {!optimistic && isPending && (
                   <div className="p-4 rounded-xl flex justify-center items-center bg-gray-100 dark:bg-gray-800 animate-pulse text-gray-400 font-mono text-sm">
                       Waiting for Server...
                   </div>
               )}
           </div>

           {/* Server Status */}
           <div className="absolute top-6 right-6">
                {isPending ? (
                    <div className="flex items-center gap-2 text-[10px] font-mono text-yellow-600 bg-yellow-100 dark:bg-yellow-900/30 px-2 py-1 rounded border border-yellow-200 dark:border-yellow-900/50">
                        <span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></span>
                        NETWORK REQ...
                    </div>
                ) : (
                    <div className="flex items-center gap-2 text-[10px] font-mono text-green-600 bg-green-100 dark:bg-green-900/30 px-2 py-1 rounded border border-green-200 dark:border-green-900/50">
                         <span className="w-2 h-2 rounded-full bg-green-500"></span>
                         IDLE
                    </div>
                )}
           </div>
      </div>

    </div>
  );
}