1import { useCallback } from 'react'; 2import { UseFormReturn } from 'react-hook-form'; 3 4import { set } from 'lodash'; 5 6interface Options<R> { 7 name: string; 8 formAPI: UseFormReturn<any>; 9 defaults?: R[]; 10 11 // if true, sets `__deleted: true` but does not remove item from the array in values 12 softDelete?: boolean; 13} 14 15export type ControlledField<R> = R & { 16 __deleted?: boolean; 17}; 18 19const EMPTY_ARRAY = [] as const; 20 21/* 22 * react-hook-form's own useFieldArray is uncontrolled and super buggy. 23 * this is a simple controlled version. It's dead simple and more robust at the cost of re-rendering the form 24 * on every change to the sub forms in the array. 25 * Warning: you'll have to take care of your own unique identiifer to use as `key` for the ReactNode array. 26 * Using index will cause problems. 27 */ 28export function useControlledFieldArray<R>(options: Options<R>) { 29 const { name, formAPI, defaults, softDelete } = options; 30 const { watch, getValues, reset, setValue } = formAPI; 31 32 const fields: Array<ControlledField<R>> = watch(name) ?? defaults ?? EMPTY_ARRAY; 33 34 const update = useCallback( 35 (updateFn: (fields: R[]) => R[]) => { 36 const values = JSON.parse(JSON.stringify(getValues())); 37 const newItems = updateFn(fields ?? []); 38 reset(set(values, name, newItems)); 39 }, 40 [getValues, name, reset, fields] 41 ); 42 43 return { 44 fields, 45 append: useCallback((values: R) => update((fields) => [...fields, values]), [update]), 46 remove: useCallback( 47 (index: number) => { 48 if (softDelete) { 49 setValue(`${name}.${index}.__deleted`, true); 50 } else { 51 update((items) => { 52 const newItems = items.slice(); 53 newItems.splice(index, 1); 54 return newItems; 55 }); 56 } 57 }, 58 [update, name, setValue, softDelete] 59 ), 60 }; 61} 62