Декларативная привязка формы

Декларативная привязка формы

1. Введение

Реализация форм для изменения данных через диалоговые окна или модальные экраны является одной из самых распространённых, но при этом сложных задач в разработке веб-приложений. Особенно это касается таких страниц, как администраторская или дашборд с 'экраном изменения детальной информации о контенте', где необходимо асинхронно искать детальные данные через бэкенд API в момент открытия экрана и связывать их с начальными значениями формы, что требует более тонкой настройки состояния.

При первоначальном проектировании компонентов мы использовали хуки useEffect — самый интуитивно понятный способ — чтобы отслеживать изменения детальных данных, переданных через внешние Props, и внедрять значения в состояние управления формой через императивный подход. Однако этот способ создал непредвиденные проблемы с параллелизмом на этапе углубления. Когда пользователь быстро закрывает диалог и выбирает другой элемент для повторного открытия, старые данные могут на короткое время оставаться в полях ввода из-за тайминга асинхронной выборки данных, или возникают ошибки, связанные с порядком вызовов хуков и жизненным циклом рендеринга компонентов, из-за чего состояние теряется.

Для решения этой хронической проблемы, ухудшающей читаемость UI-слоя и угрожающей целостности данных, мы без колебаний отказались от императивного useEffect и решили воспользоваться ключевой архитектурой библиотеки React Hook Form для достижения декларативной синхронизации состояния в рамках практического рефакторинга.

2. Предпосылки выбора технологий

Одним из распространённых антипаттернов, совершаемых при управлении состоянием форм в React, является попытка контролировать поток данных внутри useEffect. Структура, в которой определённое состояние синхронизируется вручную при каждом изменении данных, принятых через Props, имеет следующие ограничения.

  1. Несоответствие тайминга синхронизации состояния: Обновления состояния в React обрабатываются асинхронно и пакетно, поэтому перед тем как useEffect выполнится и обновит значения формы, браузер рисует экран, основываясь на предыдущем состоянии, что приводит к визуальным артефактам или ошибкам валидации.

  2. Разрастание императивного кода: Чем больше полей ввода, тем больше логики и условий для их инициализации разрознено внутри компонента, в результате чего создаются непомерно большие и трудночитаемые компоненты, превышающие 1000 строк кода.

React Hook Form предлагает декларативную опцию values, которая может преодолеть эти ограничения. Вместо того чтобы прямо описывать команды для сброса состояния внутри компонента, вы можете явно объявить источник данных, на который ссылается форма, в виде объекта, и библиотека синхронизирует его с рендерингом React, безопасно очищая остатки состояния и поддерживая новые экземпляры данных.

Таким образом, мы внедрили декларативный паттерн связывания в качестве решения, позволяющего безопасно интегрироваться в жизненный цикл компонента и максимизировать читаемость кода и пользовательский опыт разработчика.

3. Процесс реального применения и устранения неполадок

На основе концепции проектирования диалога изменения детальных данных данного проекта я объясняю процесс рефакторинга проблемной процедурной структуры в декларативную с конкретными блоками кода.

3.1 Императивная структура связывания на основе useEffect (До)

В существующей структуре, когда объект itemDetail, который является результатом API-запроса, поступает снаружи, хук useEffect внутри поддиалога выполняется каждый раз и заполняет отдельные поля формы вручную через метод setValue.

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>
  );
};

Указанная структура вызывала загрязнение данных в форме, когда скорость асинхронного получения замедлялась или пользователь быстро открывал и закрывал диалог, в моменты, когда itemDetail еще не успел дойти, предыдущие данные старого компонента оставались на форме.

3.2 Декларационная структура привязки на основе свойства values (После)

Для решения данной проблемы была внедрена реактивная свойство values внутри useForm, и интерфейс был унифицирован до структуры ModifyItemFormValue, чтобы форма могла автоматически реагировать на изменения данных.

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>
  );
};

После рефакторинга библиотека React Hook Form заменяет набор данных атомарно в соответствии со временем обновления виртуального DOM React, благодаря чему исключается возможность появления остатков предыдущих данных на экране или наличия гонки состояний, искажающих состояние формы.

4. Результаты применения и достижения

В результате широкого применения архитектуры декларационной привязки values библиотеки React Hook Form на основных слоях ввода данных в компании, включая экран редактирования отсутствующей информации, были достигнуты следующие результаты.

  1. Ноль явлений искажения состояния и ошибок остаточных UI: проблема появления остатков данных была совершенно решена, независимо от скорости вызова диалога или доступности асинхронной связи, и количество обращений из-за некорректной работы значительно улучшилось по сравнению с предыдущим периодом.

  2. Увеличение читаемости кода и продуктивности: с удалением бессмысленно объявленных конструкций useEffect внутри компонентов, которые затрудняли восприятие, количество строк кода, связанного с модальными окнами, в среднем сократилось более чем на 25%.

Снижение связи и обеспечение стабильности данных: логика обработки бизнес-форм данных (ModifyItemFormValue) была полностью декуплирована (Decoupling) с однонаправленным потоком к слою представления, обеспечив изоляцию чистых компонентов, не подверженных влиянию побочных эффектов старого состояния.

5. Заключение

Опыт работы с ошибками и рефакторингом, с которыми мы столкнулись в ходе выполнения этого проекта, наглядно продемонстрировал, насколько мощно определять зависимости в соответствии с декларативной концепцией, а не заставлять разработчика насильно управлять и отслеживать потоки изменения состояния через побочные эффекты (Side Effect) при проектировании архитектуры фронтенда.

Метод сопоставления жизненного цикла на основе свойства values библиотеки React Hook Form стал активом, который поднимает архитектурную прочность системы на новый уровень, выходя за простое исправление ошибок. Чем сложнее вводимые данные и переплетенные поля в среде корпоративного уровня, тем необходимей такие паттерны обеспечения целостности данных, и мы стремимся активнее внести вклад в снижение сложности экосистемы разработки компании, формализовав приобретенный опыт настройки декларационной архитектуры форм в стандартные методические указания технических компонентов компании.

* Ссылка

- React Hook Form - Документация API useForm

- Официальная документация React - Синхронизация с эффектами

- Официальная документация React - Вам может не понадобиться эффект

Д.Хёк

Site footer