Declarative Form Binding

Declarative Form Binding

1. Introduction

Implementing data modification forms through dialogs or modal screens in web application development is one of the most common yet complex tasks. Especially in structures like the 'Content Detail Modification Screen' of admin pages or dashboards, where detailed data needs to be asynchronously retrieved from backend APIs and bound as initial values of the form at the moment the screen opens, more precise state control is required.

During the initial component design, we adopted an imperative approach, which is the most intuitive method using the useEffect hook, to detect changes in detail data passed through external props and manually inject values into the form management state. However, this approach caused unexpected concurrency issues during the advanced stages. When a user quickly closes the dialog, selects another item, and opens it again, the timing of the asynchronous data fetching sometimes leaves remnants of previous data in the input fields, or bugs frequently occurred where the state was lost due to the mismatch between the order of hook calls and the component rendering lifecycle.

To solve this chronic problem that degrades the readability of the UI layer and threatens data consistency, we would like to share a practical refactoring case that achieved declarative state synchronization by boldly removing procedural useEffect and leveraging the core architecture of the React Hook Form library.

2. Background of Technology Choice

One of the common anti-patterns when controlling form state in React is the attempt to manage data flow within useEffect. The structure that manually synchronizes certain states every time the data received via props changes comes with the following limitations.

  1. Inconsistency in state synchronization timing: React's state updates are processed asynchronously in batches, so if useEffect is executed to update form values before the browser renders the screen based on the previous state, it results in visual artifacts or validation errors.

  2. Bloat of imperative code: As the number of input fields increases, the initialization logic and conditionals for each field get scattered throughout the component, producing bloated and hard-to-read spaghetti components that exceed 1,000 lines.

React Hook Form provides a declarative property called the values option that can overcome these limitations. Instead of explicitly writing commands to reset the state within the component, if the data source the form looks at is declared explicitly as an object, the library can synchronize with React's rendering pipeline to safely eliminate state remnants and maintain new data instances.

Thus, we decided to introduce a declarative binding pattern as a solution that maximizes code readability and developer experience while safely embedding within the component lifecycle.

3. Actual Application Process and Troubleshooting

Based on the design philosophy of the detailed information modification dialog for this project, we describe the process of refactoring the procedural structure that caused bugs into a declarative structure, along with specific code blocks.

3.1 useEffect-based imperative binding structure (Before)

In the existing structure, whenever the itemDetail object, which is the result of an API request from outside, comes in, the useEffect hook inside the sub-dialog is executed every time, manually filling the individual fields of the form through the setValue method.

export const ModifyItemDialogBefore = ({ itemDetail, onClose }) => {
  const { register, handleSubmit, setValue } = useForm({
    defaultValues: {
      title: '',
      category: '',
      description: '',
    },
  });

  // 외부 데이터가 바뀔 때마다 수동으로 상태를 주입하는 안티 패턴
  useEffect(() => {
    if (itemDetail) {
      setValue('title', itemDetail.title ?? '');
      setValue('category', itemDetail.category ?? '');
      setValue('description', itemDetail.description ?? '');
    }
  }, [itemDetail, setValue]);

  const onSubmit = (data) => {
    console.log('서버 전송 데이터:', data);
  };

  return (
    <Dialog open={true} onClose={onClose}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input {...register('title')} placeholder="제목" />
        <button type="submit"> 수정 </button>
      </form>
    </Dialog>
  );
};

The above structure caused a data pollution phenomenon where the previous data of the previous component remained on the form during the moment when the itemDetail had not yet arrived, either due to the delay in asynchronous fetching speed or when the user quickly turned the dialog on and off.

3.2 Declarative binding structure using values property (After)

To solve this, I refactored the code by introducing the reactive values property inside useForm and unifying the interface to the ModifyItemFormValue structure so that the form itself can respond to data changes.

interface ModifyItemFormValue {
  title: string;
  category: string;
  date: Dayjs | null;
  description: string;
  details: string;
}

export const ModifyItemDialogAfter = ({ itemDetail, onClose }) => {
  // values 옵션을 선언해 두면, useEffect 없이도 데이터 변경 시 내부 상태가 자동 동기화됨
  const methods = useForm<ModifyItemFormValue>({
    values: {
      date: itemDetail?.updatedAt ? dayjs(itemDetail.updatedAt) : dayjs(),
      title: itemDetail?.title ?? '',
      category: itemDetail?.category ?? '',
      description: itemDetail?.description ?? '',
      details: itemDetail?.details ?? '',
    },
  });

  const { handleSubmit, register } = methods;

  const onSubmit = handleSubmit((data: ModifyItemFormValue) => {
    const baseDate = dayjs(data.date).startOf('day').valueOf();
    console.log('정합성이 보장된 타임스탬프 데이터 전송:', baseDate);
  });

  return (
    <Dialog open={true} onClose={onClose}>
      <Form methods={methods} onSubmit={onSubmit}>
        <div className="flexColumn gap16">
          <Field.Text name="title" placeholder="제목" />
          <Field.Text name="details" placeholder="상세 내용" multiline rows={2} />
        </div>
        <button type="submit">수정 완료</button>
      </Form>
    </Dialog>
  );
};

Through this refactoring, the React Hook Form library atomically replaces the data set according to the timing of React's virtual DOM updates, fundamentally blocking race condition bugs that expose previous data remnants or mess up the form state.

4. Application results and achievements

As a result of fully spreading the declarative binding architecture of React Hook Form's values to key input form layers within the company, including the absence information modification screen, we achieved the following results.

  1. Zero state intertwining phenomenon and UI residual bugs: The exposure of data remnants has been perfectly solved regardless of the dialog calling speed or asynchronous communication availability, and the number of malfunction inquiries due to this has been significantly improved compared to before.

  2. Improved codebase readability and productivity: By removing the useEffect statements inside components that were indiscriminately declared and hindered readability, the overall number of source code lines related to the modal was slimmed down by an average of over 25%.

Reduced coupling and ensured data stability: The business form data processing (ModifyItemFormValue) logic was thoroughly decoupled in a unidirectional flow from the view layer, ensuring pure component isolation that is not affected by the side effects of the previous state.

5. Conclusion

The state intertwining bugs and refactoring experiences encountered during this project were a good opportunity to prove how powerful it is to declare dependencies clearly in accordance with the declarative philosophy provided by frameworks and libraries rather than having developers forcibly command and track the flow of state changes through side effects when designing frontend architecture.

The lifecycle mapping technique utilizing the values property of React Hook Form has become an asset that elevates the architectural robustness of the system beyond simple bug fixes. Especially in an enterprise environment where large-scale data is handled and input fields are intricately intertwined, such data consistency assurance patterns are essential, and I intend to actively contribute to reducing the complexity of the company's development ecosystem by assetizing the declarative form architecture tuning experience established this time as the standard guideline for technical components within the company.

* Reference

- React Hook Form - useForm API Document

- React Official Document - Synchronizing with Effects

- React Official Document - You Might Not Need an Effect

D.Hyeok

Site footer