Fix: Eliminating Double Async Validation in TanStack Form & Zod

Published: (February 17, 2026 at 01:10 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

A practical pattern to prevent duplicate API calls and race conditions in complex React forms.

When building production‑grade forms with TanStack Form and Zod, especially in flows involving side effects (e.g., OTP generation, user verification), you may encounter an elusive bug:

⚠️ Async validation running twice on submit

This can lead to duplicated API calls, inconsistent state, and a poor user experience.


🚨 The Problem: Double Async Execution

A known issue in TanStack Form (Issue #1431) causes async validation (superRefine) to execute multiple times during submission.

Typical Setup

const form = useForm({
  defaultValues: { /* ... */ },
  validators: {
    onChange: myZodSchema, // async superRefine
  },
  onSubmit: async ({ value }) => {
    await sendOtp(value); // ❌ may be called twice
  },
});

Why It’s Dangerous

In flows like OTP authentication:

  • Multiple requests generate different codes
  • The first code becomes invalid
  • Users get stuck

This is not just inefficiency—it’s a critical UX bug.

Root Cause

  • TanStack Form may trigger validation multiple times internally.
  • superRefine contains side effects, turning validation into a non‑pure function.
  • Validation is no longer idempotent, breaking expectations.

✅ The Solution: Take Back Control

We fix the issue with three architectural decisions.

1. Manual Validation with safeParseAsync

Avoid relying on automatic validation during submission.

const result = await myZodSchema.safeParseAsync(form.state.values);
  • Prevents double execution
  • Gives full control over the validation lifecycle

2. Prevent Re‑entrancy with useRef

React state isn’t always fast enough to block rapid interactions. Use a low‑level semaphore:

const isSubmittingRef = useRef(false);

3. Decouple Side Effects

Never trigger API calls inside validation.

  • Validation must remain pure
  • Side effects go inside a controlled submit flow

🧩 Full Implementation

import { useRef } from 'react';
import { useForm } from '@tanstack/react-form';
import { myZodSchema } from './schema';
import { triggerOtpRequest } from './api';

const isSubmittingRef = useRef(false);

const handleSubmit = async () => {
  if (isSubmittingRef.current) return; // prevent re‑entrancy

  isSubmittingRef.current = true;

  try {
    // 1. Manual validation
    const result = await myZodSchema.safeParseAsync(form.state.values);

    if (!result.success) {
      // map errors to the form if needed
      return;
    }

    // 2. Execute side effect ONCE
    await triggerOtpRequest(result.data);
  } finally {
    isSubmittingRef.current = false;
  }
};

🌐 Network Layer Optimization

During testing another issue surfaced: unwanted refetching when the user switches tabs, which disrupts the OTP flow.

Fix: Adjust QueryClient configuration

import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,          // 5 minutes
      refetchOnWindowFocus: false,      // disable automatic refetch
    },
  },
});
  • Prevents UI state resets when the user returns to the tab
  • Reduces unnecessary network traffic in critical flows

🏗️ Production Insights

  • Small validation bugs can scale into massive API waste under high traffic.
  • Race conditions are often invisible in local development.
  • Libraries may not be safe for side‑effect‑heavy flows.

Takeaway: Design defensively and keep validation pure.


🔑 Key Takeaways

  • Validation must be pure – avoid side effects inside superRefine.
  • Control execution manually – use safeParseAsync for explicit validation.
  • Prevent race conditions – employ useRef as a semaphore.
  • Tune network behavior – disable refetchOnWindowFocus when appropriate.

💬 Final Thoughts

If your validation triggers APIs, you’re no longer just validating—you’re orchestrating stateful workflows. Treat it like backend logic, not merely form validation.


0 views
Back to Blog

Related posts

Read more »