Implement useOrderedFieldArray Hook for forms using React Hook Form

Rex Ye
2 min readMay 14, 2021

Business Requirement:

I have the following requirements for my invoice entity:

1. The Invoice entity has a collection of InvoiceDetail entity.

2. User should be able to append, remove, move up and down InvoiceDetails

3. InvoiceDetail’s order needs to be consistent because they are listed in the printout of the invoice

Other documents such as contract and purchase order would have similar requirements.

The above translate to the below technical requirements:

1. On appending, set InvoiceDetail’s foreign key `InvoiceId` value to its parent Invoice’s id on appending.

2. On appending, set InvoiceDetail’s id. I use UUID for all my domain entities, and my backend expects the front end to generate UUID, and it doesn’t generate UUID automatically.

3. On appending, moving up and down, set and maintain the `order` property of InvoiceDetails automatically

4. On removing, maintain the order of the rest of InvoiceDetails.

React Hook Form has its own useFieldArray API for handling child entity collections in one-many relationships. However, for the above requirements, I decided that I would reinvent the wheels and implement my own useOrderedFieldArray hook, both as a challenge to myself and more controls potentially If I succeed.

The useOrderedFieldArray hooks would take four inputs:

1. formContext: UseFormReturn<any>
The form context we get back from React Hook form’s `useForm` hook.

2. name: string
The name of the child collection, for example, the Invoice entity has a property ‘invoiceDetails’ for its Invoice Details. The name would be this ‘invoiceDetails’

3. items: T[]
The child collection data for initialisation aka InvoiceDetails, in the Invoice case, `T` would be of type `InvoiceDetail`.

4. newItemFactory: (...args: any[]) => Partial<T>
A factory function to create a new child entity. args will be passed from the returned append method to this factory.

The useOrderedFieldArray hooks would return the following methods:

1. append: (...args: any[]) => void
Method to append new child, args will be passed to newItemFactory input method

2. moveDown: (index: number) => void
Method to move a child one step down takes the child’s index in the collection array

3. moveUp: (index: number) => void
Method to move a child one step up.

4. remove: (item: T) => void
Remove a child from the child collection.

5. fields: T[]
Similar to the fields returned by React Hook Form’s useFieldArray hook, it is to be used to render form controls

6. setFields: Dispatch<SetStateAction<T[]>>
`fields`
setter form the caller to set `fields` if appropriate.

7. updateFieldsFromContext: () => void
Method to copy data from formContext into fields. When the user copy data from a selected proforma invoice to create a new commercial invoice, this method is required to sync the child forms.

Below is the code for the hook:

Usage:

const { getValues } = formContext;

const
newItemFactory = useCallback(
() => ({ id: v4(), inoviceId: getValues('id') }),
[getValues]
);
const { fields, moveUp, moveDown, remove, append, updateFieldsFromContext } = useOrderedFieldArray({
items,
formContext,
newItemFactory,
name: 'invoiceDetails',
});

1. Use Fields to render child forms.
2. wire up helper methods to buttons.

I can confirm that the above served me well so far.

--

--