Graneet Form LogoGraneet form

Watching Form Values

Learn how to efficiently watch and react to form value changes using graneet-form's subscription-based system

Watching Form Values

Learn how to efficiently watch and react to form value changes using graneet-form's subscription-based system.

Overview

Graneet-form provides a powerful subscription system that allows components to watch specific form fields or all fields. This system is designed for optimal performance, ensuring components only re-render when their watched fields change.

Watch Modes

useFieldsWatch - Real-time Updates

Updates immediately whenever a field value changes (on every keystroke).

import { useFieldsWatch, useFormContext } from 'graneet-form';

function LiveFormDisplay() {
  const form = useFormContext<{ name: string; email: string }>();
  
  // Watch specific fields (recommended for performance)
  const { name, email } = useFieldsWatch(form, ['name', 'email']);
  
  return (
    <div>
      <p>Name: {name}</p>
      <p>Email: {email}</p>
    </div>
  );
}

useFieldsWatch with onBlur Mode - Update on Blur

Updates only when fields lose focus (after user finishes editing).

import { useFieldsWatch, useFormContext } from 'graneet-form';

function FormSummary() {
  const form = useFormContext<FormData>();
  
  // Updates only when fields lose focus
  const { title, description } = useFieldsWatch(form, ['title', 'description'], { mode: 'onBlur' });
  
  return (
    <div className="summary">
      <h3>{title}</h3>
      <p>{description}</p>
    </div>
  );
}

Watching Specific Fields vs All Fields

More performant as components only re-render when watched fields change.

// ✅ Efficient - only re-renders when name or email changes
const { name, email } = useFieldsWatch(form, ['name', 'email']);

Watching All Fields

Less performant but sometimes necessary for global form state.

// ⚠️ Less efficient - re-renders on any field change
const allValues = useFieldsWatch(form, undefined);

Practical Examples

1. Form Validation Summary

function ValidationSummary() {
  const form = useFormContext<UserForm>();
  const { name, email, password } = useFieldsWatch(form, ['name', 'email', 'password'], { mode: 'onBlur' });
  
  const completedFields = [name, email, password].filter(Boolean).length;
  const totalFields = 3;
  const progress = (completedFields / totalFields) * 100;
  
  return (
    <div className="validation-summary">
      <div className="progress">
        Form Progress: {progress.toFixed(0)}%
        <div className="progress-bar" style={{ width: `${progress}%` }} />
      </div>
      <ul>
        <li className={name ? 'complete' : 'incomplete'}>Name</li>
        <li className={email ? 'complete' : 'incomplete'}>Email</li>
        <li className={password ? 'complete' : 'incomplete'}>Password</li>
      </ul>
    </div>
  );
}

2. Auto-save Indicator

function AutoSaveStatus() {
  const form = useFormContext<ArticleForm>();
  const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved');
  
  // Watch for changes in real-time
  const { title, content } = useFieldsWatch(form, ['title', 'content']);
  
  useEffect(() => {
    setSaveStatus('unsaved');
    
    const saveTimeout = setTimeout(async () => {
      setSaveStatus('saving');
      try {
        await api.saveDraft({ title, content });
        setSaveStatus('saved');
      } catch {
        setSaveStatus('unsaved');
      }
    }, 2000);
    
    return () => clearTimeout(saveTimeout);
  }, [title, content]);
  
  return (
    <div className={`save-status ${saveStatus}`}>
      {saveStatus === 'saved' && '✅ Saved'}
      {saveStatus === 'saving' && '⏳ Saving...'}
      {saveStatus === 'unsaved' && '📝 Unsaved changes'}
    </div>
  );
}

3. Dependent Field Updates

function AddressForm() {
  const form = useFormContext<AddressFormData>();
  
  // Watch zip code changes
  const { zipCode } = useFieldsWatch(form, ['zipCode'], { mode: 'onBlur' });
  
  useEffect(() => {
    if (zipCode && zipCode.length === 5) {
      // Auto-populate city and state based on zip code
      geocodeService.getByZip(zipCode).then(info => {
        form.setFormValues({
          city: info.city,
          state: info.state
        });
      });
    }
  }, [zipCode, form]);
  
  return (
    <div>
      <Field name="zipCode" render={/* zip code input */} />
      <Field name="city" render={/* city input */} />
      <Field name="state" render={/* state input */} />
    </div>
  );
}

4. Dynamic Form Behavior

function PaymentForm() {
  const form = useFormContext<PaymentFormData>();
  
  // Watch payment method selection
  const { paymentMethod } = useFieldsWatch(form, ['paymentMethod']);
  
  return (
    <div>
      <Field<PaymentFormData, 'paymentMethod'>
        name="paymentMethod"
        render={(fieldProps) => (
          <select
            value={fieldProps.value || ''}
            onChange={(e) => fieldProps.onChange(e.target.value)}
            onBlur={fieldProps.onBlur}
            onFocus={fieldProps.onFocus}
          >
            <option value="">Select payment method</option>
            <option value="card">Credit Card</option>
            <option value="paypal">PayPal</option>
            <option value="bank">Bank Transfer</option>
          </select>
        )}
      />
      
      {/* Conditionally render payment-specific fields */}
      {paymentMethod === 'card' && <CreditCardFields />}
      {paymentMethod === 'paypal' && <PayPalFields />}
      {paymentMethod === 'bank' && <BankTransferFields />}
    </div>
  );
}

Performance Considerations

1. Watch Only What You Need

// ❌ Watching all fields when only needing name
const allValues = useFieldsWatch(form, undefined);
const name = allValues.name;

// ✅ Watch only the specific field needed
const { name } = useFieldsWatch(form, ['name']);

2. Choose Appropriate Watch Mode

// For real-time display (search, live preview, etc.)
const { searchTerm } = useFieldsWatch(form, ['searchTerm']);

// For validation, summaries, auto-save (less frequent updates)
const { title, content } = useFieldsWatch(form, ['title', 'content'], { mode: 'onBlur' });

3. Debounce Expensive Operations

function ExpensiveFormWatcher() {
  const form = useFormContext<FormData>();
  const { complexField } = useFieldsWatch(form, ['complexField']);
  
  // Debounce expensive calculations
  const debouncedValue = useDebounce(complexField, 500);
  
  useEffect(() => {
    if (debouncedValue) {
      performExpensiveCalculation(debouncedValue);
    }
  }, [debouncedValue]);
  
  return <div>...</div>;
}

Advanced Patterns

Multiple Watchers in One Component

function ComprehensiveFormWatcher() {
  const form = useFormContext<ComplexForm>();
  
  // Real-time updates for immediate feedback
  const { searchQuery } = useFieldsWatch(form, ['searchQuery']);
  
  // Blur updates for less critical information
  const { title, description, tags } = useFieldsWatch(form, ['title', 'description', 'tags'], { mode: 'onBlur' });
  
  // Global form state for debugging (use sparingly)
  const allValues = useFieldsWatch(form, undefined);
  
  return (
    <div>
      {/* Real-time search results */}
      <SearchResults query={searchQuery} />
      
      {/* Form summary updated on blur */}
      <FormPreview title={title} description={description} tags={tags} />
      
      {/* Debug panel (development only) */}
      {process.env.NODE_ENV === 'development' && (
        <pre>{JSON.stringify(allValues, null, 2)}</pre>
      )}
    </div>
  );
}

Custom Hook for Common Patterns

// Custom hook for form progress tracking
function useFormProgress<T>(form: FormContextApi<T>, requiredFields: (keyof T)[]) {
  const watchedValues = useFieldsWatch(form, requiredFields, { mode: 'onBlur' });
  
  return useMemo(() => {
    const completedFields = requiredFields.filter(
      field => watchedValues[field] != null && watchedValues[field] !== ''
    ).length;
    
    return {
      completed: completedFields,
      total: requiredFields.length,
      percentage: (completedFields / requiredFields.length) * 100,
      isComplete: completedFields === requiredFields.length
    };
  }, [watchedValues, requiredFields]);
}

// Usage
function FormProgressIndicator() {
  const form = useFormContext<UserForm>();
  const progress = useFormProgress(form, ['name', 'email', 'password']);
  
  return (
    <div>
      Progress: {progress.completed}/{progress.total} ({progress.percentage.toFixed(0)}%)
    </div>
  );
}

Global Watching Warnings

Performance Impact

Global watching (useFieldsWatch(form, undefined)) causes components to re-render on every form change. Use sparingly and only when necessary.

Best Practice

Always prefer specific field watching over global watching. If you need many fields, consider splitting your component or using multiple targeted watchers.

Migration from Other Form Libraries

From React Hook Form

// React Hook Form
const { watch } = useForm();
const name = watch('name');
const allValues = watch();

// Graneet Form equivalent
const form = useFormContext<FormType>();
const { name } = useFieldsWatch(form, ['name']);
const allValues = useFieldsWatch(form, undefined);

From Formik

// Formik
const { values } = useFormikContext<FormType>();
const name = values.name;

// Graneet Form equivalent
const form = useFormContext<FormType>();
const { name } = useFieldsWatch(form, ['name']);