feat: add useConditionalField hook and implement reactive form showcase examples

This commit is contained in:
Firman Ramdhani
2026-06-15 22:03:11 +07:00
parent f7c7bc6907
commit 50cce32ada
14 changed files with 862 additions and 172 deletions
+1
View File
@@ -1 +1,2 @@
export * from '@mantine/hooks';
export * from './useConditionalField';
@@ -0,0 +1,69 @@
import { useEffect } from 'react';
import type { UseFormSetValue, UseFormUnregister, UseFormClearErrors, FieldValues, Path } from 'react-hook-form';
export interface UseConditionalFieldOptions<TFieldValues extends FieldValues> {
condition: boolean;
name: Path<TFieldValues>;
setValue: UseFormSetValue<TFieldValues>;
unregister?: UseFormUnregister<TFieldValues>;
clearErrors?: UseFormClearErrors<TFieldValues>;
defaultValue?: any;
mode?: 'unregister' | 'reset';
}
/**
* Automatically cleans up a conditionally rendered React Hook Form field
* when its parent condition becomes false.
*
* @param options Configuration object for the conditional field behavior
*/
export function useConditionalField<TFieldValues extends FieldValues>(
options: UseConditionalFieldOptions<TFieldValues>
) {
// Destructure with default values
const {
condition,
name,
setValue,
unregister,
clearErrors,
defaultValue,
mode = 'unregister'
} = options;
const config = options;
useEffect(() => {
// When the condition evaluates to false, we execute the cleanup logic
if (!condition) {
// 1. Reset the field value. We use a stable empty state (like '' instead of undefined)
// to prevent uncontrolled component fallback in the React UI. We also force RHF to sync.
const targetValue = config.defaultValue !== undefined ? config.defaultValue : ('' as any);
config.setValue(config.name, targetValue, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
// 2. Execute the appropriate side-effect strategy based on the active mode
if (mode === 'unregister' && config.unregister) {
// Unregister Mode: Completely unmounts the field from React Hook Form.
// The value is removed from the payload and validation is entirely bypassed.
config.unregister(config.name);
} else if (mode === 'reset' && config.clearErrors) {
// Reset Mode: Keeps the field active in the DOM (e.g. cascading or disabled dependencies).
// Wipes the value and clears active validation errors so the user can interact
// with a fresh state, but keeps the property in the payload.
config.clearErrors(config.name);
}
}
}, [
condition,
name,
setValue,
unregister,
clearErrors,
defaultValue,
mode
]);
}
@@ -1,4 +1,4 @@
import { z, type ZodString, type ZodNumber, type ZodTypeAny } from 'zod';
import type { ZodString, ZodNumber, ZodTypeAny } from 'zod';
// ─── UTILITIES ─────────────────────────────────────────────────────────────