Skip to content

Form Handling with React Query

The Golden Rule

Never drive form inputs directly from a Mutation. Use Local State for the UI, and only sync to Server State (React Query) on specific events (Submit, Blur, or Debounce).

The Problem: "The Optimistic Race"

When you bind an input directly to a React Query mutation:

// ❌ BAD PATTERN
<input
  value={serverData.value}
  onChange={(e) => mutation.mutate(e.target.value)}
/>
  1. User types "A": Mutation fires.
  2. Optimistic Update: Cache updates to "A".
  3. User types "B": Mutation fires.
  4. Race Condition: If the first mutation settles (or validates) and triggers a refetch before the second keystroke is processed, the input value might reset or "jump" based on the server response.
  5. Performance: You are spamming the network with one request per keystroke.

The Solution: "Buffered Mutation"

Use local useState to buffer the user's input. Only trigger the mutation when the user is "done".

// ✅ GOOD PATTERN
function SettingsModal({ serverData, saveToServer }) {
  // 1. Initialize local state from server data
  const [localValue, setLocalValue] = useState(serverData.value);

  // 2. Drive the Input from Local State (Instant, 0 latency)
  // 3. Sync to Server on "Done" (or debounce)
  const handleSave = () => saveToServer(localValue);

  return (
    <>
      <input 
        value={localValue} 
        onChange={(e) => setLocalValue(e.target.value)} 
      />
      <button onClick={handleSave}>Done</button>
    </>
  );
}

When to use this?

  • Modals / Settings: When there is a clear "Save" or "Done" button.
  • Complex Forms: When validation needs to happen locally before hitting the API.
  • High-Frequency Inputs: Sliders, text fields, or steppers where every intermediate value doesn't need to be persisted.

Key Implementation Details

  1. Sync on Mount/Change: If the server data changes externally (e.g., background refetch), update the local state.

    useEffect(() => {
        setLocalValue(serverData.value);
    }, [serverData]);
    
  2. Optimistic Updates: You can still use optimistic updates in your useMutation hook. Because the input is driven by local state, the UI won't glitch even if the server state "jumps" during the update cycle.