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