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
Watching Specific Fields (Recommended)
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']);