Files
2025-11-30 08:25:27 +08:00

7.7 KiB

Top 12 Common Errors with Solutions

Complete reference for known issues and their solutions.


1. Zod v4 Type Inference Errors

Error: Type inference doesn't work correctly with Zod v4

Symptoms:

// Types don't match expected structure
const schema = z.object({ name: z.string() })
type FormData = z.infer<typeof schema> // Type issues

Source: GitHub Issue #13109 (Closed 2025-11-01)

Note: This issue was resolved in react-hook-form v7.66.x. Upgrade to v7.66.1+ to avoid this problem.

Solution:

// Use correct Zod v4 patterns
const schema = z.object({ name: z.string() })
type FormData = z.infer<typeof schema>

// Explicitly type useForm if needed
const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
})

2. Uncontrolled to Controlled Warning

Error: "A component is changing an uncontrolled input to be controlled"

Symptoms:

Warning: A component is changing an uncontrolled input of type text to be controlled.
Input elements should not switch from uncontrolled to controlled (or vice versa).

Cause: Not setting defaultValues causes fields to be undefined initially

Solution:

// BAD
const form = useForm()

// GOOD - Always set defaultValues
const form = useForm({
  defaultValues: {
    email: '',
    password: '',
    remember: false,
  },
})

3. Nested Object Validation Errors

Error: Errors for nested fields don't display correctly

Symptoms:

// errors.address.street is undefined even though validation failed
<span>{errors.address.street?.message}</span> // Shows nothing

Solution:

// Use optional chaining for nested errors
{errors.address?.street && (
  <span>{errors.address.street.message}</span>
)}

// OR check if errors.address exists first
{errors.address && errors.address.street && (
  <span>{errors.address.street.message}</span>
)}

4. Array Field Re-renders

Error: Form re-renders excessively with useFieldArray

Cause: Using array index as key instead of field.id

Solution:

// BAD
{fields.map((field, index) => (
  <div key={index}> {/* Using index causes re-renders */}
    ...
  </div>
))}

// GOOD
{fields.map((field) => (
  <div key={field.id}> {/* Use field.id */}
    ...
  </div>
))}

5. Async Validation Race Conditions

Error: Multiple validation requests cause conflicting results

Symptoms: Old validation results override new ones

Solution:

// Use debouncing
import { useDebouncedCallback } from 'use-debounce'

const debouncedValidation = useDebouncedCallback(
  () => trigger('username'),
  500 // Wait 500ms after user stops typing
)

// AND cancel pending requests
const abortControllerRef = useRef<AbortController | null>(null)

useEffect(() => {
  if (abortControllerRef.current) {
    abortControllerRef.current.abort()
  }

  abortControllerRef.current = new AbortController()

  // Make request with abort signal
  fetch('/api/check', { signal: abortControllerRef.current.signal })
}, [value])

6. Server Error Mapping

Error: Server validation errors don't map to form fields

Solution:

const onSubmit = async (data) => {
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data),
    })

    if (!response.ok) {
      const { errors } = await response.json()

      // Map server errors to form fields
      Object.entries(errors).forEach(([field, message]) => {
        setError(field, {
          type: 'server',
          message: Array.isArray(message) ? message[0] : message,
        })
      })

      return
    }
  } catch (error) {
    setError('root', {
      type: 'server',
      message: 'Network error',
    })
  }
}

7. Default Values Not Applied

Error: Form fields don't show default values

Cause: Setting defaultValues after form initialization

Solution:

// BAD - Set in useState
const [defaultValues, setDefaultValues] = useState({})

useEffect(() => {
  setDefaultValues({ email: 'user@example.com' }) // Too late!
}, [])

const form = useForm({ defaultValues })

// GOOD - Set directly or use reset()
const form = useForm({
  defaultValues: { email: 'user@example.com' },
})

// OR fetch and use reset
useEffect(() => {
  async function loadData() {
    const data = await fetchData()
    reset(data)
  }
  loadData()
}, [reset])

8. Controller Field Not Updating

Error: Custom component doesn't update when value changes

Cause: Not spreading {...field} in Controller render

Solution:

// BAD
<Controller
  render={({ field }) => (
    <CustomInput value={field.value} onChange={field.onChange} />
  )}
/>

// GOOD - Spread all field props
<Controller
  render={({ field }) => (
    <CustomInput {...field} />
  )}
/>

9. useFieldArray Key Warnings

Error: React warning about duplicate keys in list

Symptoms:

Warning: Encountered two children with the same key

Solution:

// BAD - Using index as key
{fields.map((field, index) => (
  <div key={index}>...</div>
))}

// GOOD - Use field.id
{fields.map((field) => (
  <div key={field.id}>...</div>
))}

10. Schema Refinement Error Paths

Error: Custom validation errors appear at wrong field

Cause: Not specifying path in refinement

Solution:

// BAD - Error appears at form level
z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  // Missing path!
})

// GOOD - Error appears at confirmPassword field
z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword'], // Specify path
})

11. Transform vs Preprocess Confusion

Error: Data transformation doesn't work as expected

When to use each:

// Use TRANSFORM for output transformation (after validation)
z.string().transform((val) => val.toUpperCase())
// Input: 'hello' -> Validation: passes -> Output: 'HELLO'

// Use PREPROCESS for input transformation (before validation)
z.preprocess(
  (val) => (val === '' ? undefined : val),
  z.string().optional()
)
// Input: '' -> Preprocess: undefined -> Validation: passes

12. Multiple Resolver Conflicts

Error: Form validation doesn't work with multiple resolvers

Cause: Trying to use multiple validation libraries simultaneously

Solution:

// BAD - Can't use multiple resolvers
const form = useForm({
  resolver: zodResolver(schema),
  resolver: yupResolver(schema), // Overrides previous
})

// GOOD - Use single resolver, combine schemas if needed
const schema1 = z.object({ email: z.string() })
const schema2 = z.object({ password: z.string() })
const combinedSchema = schema1.merge(schema2)

const form = useForm({
  resolver: zodResolver(combinedSchema),
})

Debugging Tips

Enable DevTools

npm install @hookform/devtools
import { DevTool } from '@hookform/devtools'

<DevTool control={control} />

Log Form State

useEffect(() => {
  console.log('Form State:', formState)
  console.log('Errors:', errors)
  console.log('Values:', getValues())
}, [formState, errors, getValues])

Validate on Change During Development

const form = useForm({
  mode: 'onChange', // See errors immediately
  resolver: zodResolver(schema),
})

Official Docs: